├── .gitignore ├── .htaccess ├── License ├── Makefile ├── README.md ├── Vagrantfile ├── about.cgi ├── access_control.rb ├── accessibility.css ├── advanced.rb ├── atom.rb ├── authorization_required.cgi ├── charset.rb ├── check.cgi ├── classic.css ├── config_defaults.rb ├── config_sample.rb ├── customize.cgi ├── customize.rb ├── date_locale.rb ├── default.css ├── delete_poll.rb ├── dudle.rb ├── dudle_cleanup.sh ├── edit_columns.rb ├── error.cgi ├── example.cgi ├── favicon.ico ├── hash.rb ├── history.rb ├── html.rb ├── index.cgi ├── invite_participants.rb ├── locale ├── ar │ └── dudle.po ├── bg │ └── dudle.po ├── ca │ └── dudle.po ├── cs │ └── dudle.po ├── da │ └── dudle.po ├── de │ └── dudle.po ├── eo │ └── dudle.po ├── es │ └── dudle.po ├── es_AR │ └── dudle.po ├── et │ └── dudle.po ├── fi │ └── dudle.po ├── fr │ └── dudle.po ├── gl │ └── dudle.po ├── he │ └── dudle.po ├── hu │ └── dudle.po ├── it │ └── dudle.po ├── ln │ └── dudle.po ├── nl │ └── dudle.po ├── no │ └── dudle.po ├── pl │ └── dudle.po ├── pt_BR │ └── dudle.po ├── ru │ └── dudle.po ├── sv │ └── dudle.po ├── sw │ └── dudle.po └── tr │ └── dudle.po ├── log.rb ├── maintenance.cgi ├── not_found.cgi ├── overview.rb ├── participate.rb ├── poll.rb ├── pollhead.rb ├── print.css ├── timepollhead.rb ├── timestring.rb ├── vcs_git.rb └── vcs_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | config.rb 2 | locale/dudle.pot 3 | locale/*/dudle.mo 4 | css/* 5 | extensions/* 6 | maintenance.html 7 | .vagrant 8 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | #RewriteCond %{REMOTE_ADDR} !127.0.0.1 3 | RewriteCond %{DOCUMENT_ROOT}/maintenance.cgi -f 4 | RewriteCond %{DOCUMENT_ROOT}/maintenance.html -f 5 | RewriteCond %{SCRIPT_FILENAME} !maintenance.cgi 6 | RewriteCond %{SCRIPT_FILENAME} !css/* 7 | RewriteCond %{SCRIPT_FILENAME} !.*\.css 8 | RewriteCond %{SCRIPT_FILENAME} !.*\.po 9 | RewriteCond %{SCRIPT_FILENAME} !favicon.ico 10 | RewriteRule ^.*$ /maintenance.cgi [R=503,L] 11 | ErrorDocument 503 /maintenance.cgi 12 | 13 | SetEnv LC_ALL "en_US.UTF-8" 14 | 15 | 16 | deny from all 17 | satisfy all 18 | ErrorDocument 403 "Access to these files is forbidden!" 19 | 20 | 21 | 22 | deny from all 23 | satisfy all 24 | ErrorDocument 403 "Access to these files is forbidden!" 25 | 26 | 27 | RewriteRule \.git.* /data.yaml 28 | 29 | Options +ExecCGI 30 | AddHandler cgi-script .cgi 31 | DirectoryIndex index.cgi 32 | 33 | ErrorDocument 500 /error.cgi 34 | ErrorDocument 404 /not_found.cgi 35 | ErrorDocument 401 /authorization_required.cgi 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | .DELETE_ON_ERROR: 21 | .POSIX: 22 | 23 | DOMAIN=dudle 24 | INSTALL=install -p 25 | INSTALL_DATA=$(INSTALL) -m 644 26 | prefix=/usr/local 27 | datadir=$(prefix)/share 28 | localstatedir=$(prefix)/var 29 | 30 | .PHONY: locale 31 | locale: $(foreach p,$(wildcard locale/*/$(DOMAIN).po), $(addsuffix .mo,$(basename $p))) 32 | 33 | RGETTEXT=$(firstword $(shell which rgettext rxgettext)) 34 | 35 | locale/$(DOMAIN).pot: *.rb *.cgi 36 | rm -f $@ 37 | $(RGETTEXT) *.cgi *.rb -o $@ 38 | 39 | %.mo: %.po 40 | msgfmt $*.po -o $*.mo 41 | 42 | locale/%/$(DOMAIN).po: locale/$(DOMAIN).pot 43 | msgmerge $@ $? >/tmp/$(DOMAIN)_$*_tmp.po 44 | if [ "`msgcomm -u /tmp/$(DOMAIN)_$*_tmp.po $@`" ];then\ 45 | mv /tmp/$(DOMAIN)_$*_tmp.po $@;\ 46 | else\ 47 | touch $@;\ 48 | fi 49 | @if [ "`potool -fnt $@ -s`" != "0" -o "`potool -ff $@ -s`" != "0" ];then\ 50 | echo "WARNING: There are untranslated Strings in $@";\ 51 | if [ "X:$$DUDLE_POEDIT_AUTO" = "X:$*" ]; then\ 52 | poedit $@;\ 53 | fi;\ 54 | fi 55 | 56 | .PHONY: install 57 | install: locale 58 | $(INSTALL) -d $(DESTDIR)$(localstatedir)/lib/$(DOMAIN) 59 | for f in about.cgi access_control.rb advanced.rb atom.rb \ 60 | authorization_required.cgi check.cgi customize.cgi \ 61 | customize.rb delete_poll.rb edit_columns.rb error.cgi \ 62 | example.cgi history.rb index.cgi invite_participants.rb \ 63 | maintenance.cgi not_found.cgi overview.rb participate.rb; do \ 64 | $(INSTALL) -D -t $(DESTDIR)$(datadir)/$(DOMAIN) $$f; \ 65 | ln -s $$(realpath --relative-to=$(DESTDIR)$(localstatedir)/lib/$(DOMAIN) $(DESTDIR)$(datadir)/$(DOMAIN))/$$f $(DESTDIR)$(localstatedir)/lib/$(DOMAIN)/$$f; \ 66 | done 67 | for f in .htaccess charset.rb classic.css config_defaults.rb \ 68 | date_locale.rb default.css dudle.rb favicon.ico hash.rb \ 69 | html.rb log.rb poll.rb pollhead.rb print.css timepollhead.rb \ 70 | timestring.rb vcs_git.rb vcs_test.rb; do \ 71 | $(INSTALL_DATA) -D -t $(DESTDIR)$(datadir)/$(DOMAIN) $$f; \ 72 | ln -s $$(realpath --relative-to=$(DESTDIR)$(localstatedir)/lib/$(DOMAIN) $(DESTDIR)$(datadir)/$(DOMAIN))/$$f $(DESTDIR)$(localstatedir)/lib/$(DOMAIN)/$$f; \ 73 | done 74 | for mo in locale/*/$(DOMAIN).mo; do \ 75 | lang=$$(dirname $$mo); \ 76 | $(INSTALL_DATA) -D -t $(DESTDIR)$(datadir)/$(DOMAIN)/$$lang $$lang/$(DOMAIN).mo; \ 77 | done 78 | $(INSTALL) -d $(DESTDIR)$(localstatedir)/lib/$(DOMAIN)/$$lang; \ 79 | ln -s $$(realpath --relative-to=$(DESTDIR)$(localstatedir)/lib/$(DOMAIN) $(DESTDIR)$(datadir)/$(DOMAIN))/locale $(DESTDIR)$(localstatedir)/lib/$(DOMAIN)/locale; \ 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | * Benjamin Kellermann 3 | * a lot of contributors of small code pieces 4 | 5 | # License 6 | GNU AGPL v3 or higher (see file License) 7 | 8 | # Requirements 9 | * ruby >=1.9 10 | * git >=1.6.5 11 | * ruby-gettext (for localization) 12 | * gettext, potool, make (optional, if you want to generate localization files) 13 | 14 | # Installation 15 | 1. Place this application into a directory where cgi-scripts are evaluated. 16 | 2. If you want to change some configuration, state it in the file `config.rb` 17 | (see `config_sample.rb` for help) 18 | to start with a default configuration. 19 | 3. The webserver needs the permission to write into the directory 20 | 4. You need `.mo` files in order to use localisation. 21 | You have 2 possibilities: 22 | 1. Run this small script to fetch the files from the main server: 23 | 24 | ```sh 25 | cd $DUDLE_INSTALLATION_PATH 26 | for i in locale/??; do 27 | wget -O $i/dudle.mo https://dudle.inf.tu-dresden.de/locale/`basename $i`/dudle.mo 28 | done 29 | ``` 30 | 2. Build them on your own. This requires gettext, 31 | ruby-gettext, potool, and make to be installed. 32 | 33 | ```sh 34 | sudo aptitude install ruby-gettext potool make 35 | make 36 | ``` 37 | 5. In order to let access control work correctly, the webserver needs 38 | auth_digest support. It therefore may help to type: 39 | 40 | ```sh 41 | sudo a2enmod auth_digest 42 | ``` 43 | 6. In order to get atom-feed support you need ruby-ratom to be 44 | installed. E.g.: 45 | 46 | ```sh 47 | sudo aptitude install ruby-dev libxml2-dev zlib1g-dev 48 | sudo gem install ratom 49 | ``` 50 | 7. To make titles with umlauts working you need to check the encoding in 51 | .htaccess, e.g. 52 | 53 | ```sh 54 | SetEnv LC_ALL "en_US.UTF-8" 55 | ``` 56 | 8. It might be the case, that you have to set some additional Variables 57 | in your .htaccess: 58 | 59 | ```sh 60 | SetEnv GIT_AUTHOR_NAME="http user" 61 | SetEnv GIT_AUTHOR_EMAIL=foo@example.org 62 | SetEnv GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" 63 | SetEnv GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" 64 | ``` 65 | 9. If you installed dudle to a subdirectory (i.e. http://$YOUR_SERVER/$SOMEDIR/...), 66 | than you want to adopt the ErrorDocument directives in your .htaccess. 67 | (You need an absolute path here!) 68 | 10. Try to open http://$YOUR_SERVER/check.cgi to check if your config 69 | seems to work. 70 | 11. You may want to install a cronjob to cleanup dudle polls. 71 | See dudle_cleanup.sh for an example. 72 | 12. You can enable a maintenance mode if you need to change something 73 | with your installation. Using this mode, all users will see a static 74 | message and are not able to change anything. In order to enable this 75 | mode, create a file named `/maintenance.html` which may include a 76 | custom message. E.g.: 77 | 78 | ```sh 79 | echo "
We are updating the servers and expect to be online on 0 am, January 1st, 1970 (UTC).
" > $DUDLE_INSTALLATION_PATH/maintenance.html 80 | ``` 81 | 82 | # image 83 | There is a docker image available 84 | * https://github.com/jpkorva/dudle-docker 85 | 86 | # Pimp your Installation 87 | * If you want to create your own Stylesheet, you just have to put it in 88 | the folder `$DUDLE_HOME_FOLDER/css/`. Afterwards you may config this 89 | one to be the default Stylesheet. 90 | You can fetch the whole repo from https://github.com/kellerben/dudle-css 91 | * If you want to extend the functionality you might want to place a file 92 | `main.rb` in `$DUDLE_HOME_FOLDER/extension/$YOUR_EXTENSION/main.rb` 93 | You can clone the whole sourcecode here: 94 | - https://github.com/kellerben/dudle-extensions-participate 95 | - https://github.com/kellerben/dudle-extensions-symcrypt 96 | - https://github.com/kellerben/dudle-extensions-asymcrypt 97 | - https://github.com/kellerben/dudle-extensions-gpgauth 98 | - https://github.com/kellerben/dudle-extensions-anonymous 99 | Note, that extensions are loaded in alphabetic order! The symcrypt 100 | extension e.g. needs the participate extension and therefore you need 101 | to name it “10-participate” in order to get executed first. 102 | 103 | # Translators 104 | If you set `$DUDLE_POEDIT_AUTO` to your lang, poedit will launch 105 | automatically when building the application. E.g.: 106 | 107 | ```sh 108 | export DUDLE_POEDIT_AUTO=fr 109 | git pull 110 | make # will launch poedit if new french strings are to be translated 111 | ``` 112 | 113 | * To add a new translation 114 | - first add a new folder for your language under $DUDLE_HOME_FOLDER/locale, 115 | - copy the dudle.pot file into your folder and name it dudle.po 116 | - translate sentences and phrases in your dudle.po file 117 | - add an entry for your language in dudle/dudle.rb at line 245 118 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.define "dudle" 6 | 7 | config.vm.network "forwarded_port", guest: 80, host: 8080 8 | config.vm.box = "bento/debian-11" 9 | 10 | config.vm.hostname = "dudle" 11 | ENV['LC_ALL']="en_US.UTF-8" 12 | 13 | config.vm.provision "shell", inline: <<-END 14 | apt-get install -y apache2 15 | apt-get install -y ruby ruby-gettext git 16 | apt-get install -y gettext potool make 17 | cd /vagrant/ 18 | cat > /etc/apache2/sites-available/000-default.conf < 20 | ServerAdmin webmaster@localhost 21 | DocumentRoot /var/www/html/ 22 | ErrorLog ${APACHE_LOG_DIR}/error.log 23 | CustomLog ${APACHE_LOG_DIR}/access.log combined 24 | SetEnv GIT_AUTHOR_NAME="http user" 25 | SetEnv GIT_AUTHOR_EMAIL=foo@example.org 26 | SetEnv GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" 27 | SetEnv GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" 28 | 29 | AllowOverride All 30 | 31 | 32 | FILE 33 | for i in * .htaccess; do 34 | sudo ln -s /vagrant/$i /var/www/html/ 35 | done 36 | cd /var/www/html/ 37 | make 38 | a2enmod auth_digest 39 | a2enmod rewrite 40 | systemctl restart apache2 41 | apt-get install -y ruby-dev libxml2-dev zlib1g-dev 42 | apt-get install -y gcc 43 | gem install ratom 44 | END 45 | end 46 | -------------------------------------------------------------------------------- /about.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | require_relative 'dudle' 25 | 26 | $d = Dudle.new 27 | 28 | $d << '

' 29 | $d << (format(_('This application is powered by %s.'), DuDPoll: "DuD-Poll")) 30 | $d << '

' 31 | $d << ('

' + _('License') + '

') 32 | $d << '

' 33 | $d << _("The sourcecode of this application is available under the terms of AGPL Version 3.") 34 | $d << (format(_('The sourcecode of this application can be found here: %ssource code of this application%s.'), a_start: "", a_end: '')) 35 | $d << '

' 36 | 37 | $d << $conf.aboutnotice 38 | 39 | $d.out 40 | end 41 | -------------------------------------------------------------------------------- /access_control.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ############################################################################ 3 | # Copyright 2009-2019 Benjamin Kellermann # 4 | # # 5 | # This file is part of Dudle. # 6 | # # 7 | # Dudle is free software: you can redistribute it and/or modify it under # 8 | # the terms of the GNU Affero General Public License as published by # 9 | # the Free Software Foundation, either version 3 of the License, or # 10 | # (at your option) any later version. # 11 | # # 12 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 13 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 14 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 15 | # License for more details. # 16 | # # 17 | # You should have received a copy of the GNU Affero General Public License # 18 | # along with dudle. If not, see . # 19 | ############################################################################ 20 | 21 | if __FILE__ == $0 22 | 23 | load '../dudle.rb' 24 | require 'digest' 25 | 26 | $d = Dudle.new 27 | 28 | acusers = {} 29 | 30 | File.open('.htdigest', 'r').each_line { |l| 31 | user, realm = l.scan(/^(.*):(.*):.*$/).flatten 32 | acusers[user] = realm 33 | } 34 | 35 | def write_htaccess(acusers) 36 | File.open('.htaccess', 'w') { |htaccess| 37 | if acusers.include?('admin') 38 | htaccess << < 40 | AuthType digest 41 | AuthName "dudle-#{$d.urlsuffix.gsub('"', '\\\\"')}" 42 | AuthUserFile "#{File.expand_path('.').gsub('"', '\\\\"')}/.htdigest" 43 | Require user admin 44 | ErrorDocument 401 #{$cgi.script_name.gsub(%r{[^/]*/[^/]*$}, '')}authorization_required.cgi?user=admin&poll=#{CGI.escape($d.urlsuffix)} 45 | 46 | HTACCESS 47 | end 48 | if acusers.include?('participant') 49 | htaccess << <<~HTACCESS 50 | AuthType digest 51 | AuthName "dudle-#{$d.urlsuffix.gsub('"', '\\\\"')}" 52 | AuthUserFile "#{File.expand_path('.').gsub('"', '\\\\"')}/.htdigest" 53 | Require valid-user 54 | ErrorDocument 401 #{$cgi.script_name.gsub(%r{[^/]*/[^/]*$}, '')}authorization_required.cgi?user=participant&poll=#{CGI.escape($d.urlsuffix)} 55 | HTACCESS 56 | end 57 | } 58 | VCS.commit('Access Control changed') 59 | return if acusers.empty? 60 | 61 | $d.html.header['status'] = 'REDIRECT' 62 | $d.html.header['Cache-Control'] = 'no-cache' 63 | $d.html.header['Location'] = 'access_control.cgi' 64 | end 65 | 66 | def add_to_htdigest(user, password) 67 | File.open('.htdigest', 'a') { |f| 68 | f << "#{user}:dudle-#{$d.urlsuffix.gsub(':', '\\:')}:#{Digest::MD5.hexdigest("#{user}:dudle-#{$d.urlsuffix.gsub(':', '\\:')}:#{password}")}\n" 69 | } 70 | end 71 | 72 | def createform(userarray, hint, acusers) 73 | usernamestr = _('Username:') 74 | ret = <
76 | 77 | 78 | 79 | 83 | 84 | FORM 85 | 86 | passwdstr = _('Password') 87 | repeatstr = _('repeat') 88 | 2.times { |i| 89 | ret += < 91 | 92 | ' 100 | } 101 | 102 | ret += < 104 | 105 | 106 | 107 | 108 | 109 | 126 | 127 |
#{usernamestr} 80 | #{userarray[0]} 81 | 82 |
93 | PASS 94 | if acusers.include?(userarray[0]) 95 | ret += PASSWORDSTAR * 14 96 | else 97 | ret += "" 98 | end 99 | ret += '
#{acusers.include?(userarray[0]) ? '' : hint}
110 | FORM 111 | if acusers.include?(userarray[0]) 112 | if userarray[0] == 'admin' && acusers.include?('participant') 113 | ret += "
" + _('You have to remove the participant user before you can remove the administrator.') + '
' 114 | else 115 | ret += "" 116 | ret += "" 117 | end 118 | else 119 | ret += "" 120 | ret += "" 121 | end 122 | 123 | ret += < 125 |
128 |
129 | FORM 130 | ret 131 | end 132 | 133 | if $cgi.include?('ac_user') 134 | user = $cgi['ac_user'] 135 | if user !~ /^[\w]*$/ 136 | # add user 137 | usercreatenotice = "
" + _('Only letters and digits are allowed in the username.') + '
' 138 | elsif $cgi['ac_password0'] != $cgi['ac_password1'] 139 | usercreatenotice = "
" + _('Passwords did not match.') + '
' 140 | else 141 | if $cgi.include?('ac_create') 142 | add_to_htdigest(user, $cgi['ac_password0']) 143 | acusers[user] = true 144 | write_htaccess(acusers) 145 | end 146 | 147 | # delete user 148 | deleteuser = '' 149 | acusers.each { |user, _action| 150 | if $cgi.include?("ac_delete_#{user}") 151 | deleteuser = user 152 | end 153 | } 154 | acusers.delete(deleteuser) 155 | htdigest = [] 156 | File.open('.htdigest', 'r') { |file| 157 | htdigest = file.readlines 158 | } 159 | File.open('.htdigest', 'w') { |f| 160 | htdigest.each { |line| 161 | f << line unless line =~ /^#{deleteuser}:/ 162 | } 163 | } 164 | File.chmod(0o600, '.htdigest') 165 | write_htaccess(acusers) 166 | end 167 | end 168 | 169 | $d.wizzard_redirect 170 | 171 | if $d.html.header['status'] != 'REDIRECT' 172 | 173 | $d.html << ('

' + _('Change access control settings') + '

') 174 | 175 | if acusers.empty? && $cgi['ac_activate'] != 'Activate' 176 | 177 | acstatus = ['rgb(179, 0, 0)', _('not activated')] 178 | acswitchbutton = "" 179 | acswitchbutton += "" 180 | else 181 | if acusers.empty? 182 | acstatus = ['blue', _('controls will be activated when at least the admin user is configured')] 183 | acswitchbutton = "" 184 | acswitchbutton += "" 185 | else 186 | acstatus = ['rgb(0, 120, 0)', _('activated')] 187 | acswitchbutton = "
" + _('You have to remove all users before you can deactivate the access control settings.') + '
' 188 | end 189 | 190 | admincreatenotice = usercreatenotice || _('You will be asked for the password you have entered here after you press save!') 191 | 192 | user = ['admin', 193 | _('The user ‘admin’ has access to both the vote and the configuration interface.')] 194 | 195 | createform = createform(user, admincreatenotice, acusers) 196 | if acusers.include?('admin') 197 | participantcreatenotice = usercreatenotice || '' 198 | user = ['participant', 199 | _('The user ‘participant’ only has access to the vote interface.')] 200 | createform += createform(user, participantcreatenotice, acusers) 201 | end 202 | 203 | end 204 | 205 | acstr = _('Access control:') 206 | $d.html << < 208 | 209 | 210 | 213 | 216 | 217 | 218 | 219 | 222 | 223 |
211 | #{acstr} 212 | 214 | #{acstatus[1]} 215 |
220 | #{acswitchbutton} 221 |
224 | 225 | 226 | #{createform} 227 | AC 228 | 229 | end 230 | 231 | $d.out 232 | end 233 | -------------------------------------------------------------------------------- /accessibility.css: -------------------------------------------------------------------------------- 1 | .visually-hidden { 2 | position: absolute !important; 3 | width: 1px !important; 4 | height: 1px !important; 5 | padding: 0 !important; 6 | margin: -1px !important; 7 | overflow: hidden !important; 8 | clip: rect(0,0,0,0) !important; 9 | white-space: nowrap !important; 10 | border: 0 !important; 11 | } 12 | 13 | td.undecided { 14 | color: rgb(0, 0, 0); 15 | } 16 | 17 | input.disabled { 18 | color:rgb(117, 117, 117) 19 | } 20 | 21 | .warning, .error{ 22 | color: rgb(177, 48, 16); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /advanced.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2017-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | load '../dudle.rb' 24 | if $cgi.include?('undo_revision') && $cgi['undo_revision'].to_i < VCS.revno 25 | undorevision = $cgi['undo_revision'].to_i 26 | $d = Dudle.new(revision: undorevision) 27 | comment = $cgi.include?('redo') ? 'Redo changes' : 'Reverted Poll' 28 | $d.table.store("#{comment} to version #{undorevision}") 29 | $d << ('

' + _('Revert poll') + '

') 30 | $d << (format(_('Poll was reverted to Version %s!'), version: undorevision)) 31 | else 32 | $d = Dudle.new 33 | $d << ('

' + _('Revert poll') + '

') 34 | $d << "
" 35 | $d << _('Revert poll to version (see ‘History’ tab for revision numbers): ') 36 | $d << "" 37 | $d << "" 38 | $d << '
' 39 | end 40 | 41 | $d.out 42 | end 43 | -------------------------------------------------------------------------------- /atom.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | require 'rubygems' 23 | require 'atom' 24 | require 'yaml' 25 | require 'cgi' 26 | require 'time' 27 | 28 | $cgi = CGI.new 29 | 30 | feed = Atom::Feed.new 31 | olddir = File.expand_path('.') 32 | Dir.chdir('..') 33 | def _(string) 34 | string 35 | end 36 | require_relative 'config_defaults' 37 | require_relative 'poll' 38 | Dir.chdir(olddir) 39 | 40 | poll = YAML.load_file('data.yaml') 41 | 42 | feed.title = poll.name 43 | feed.id = "urn:dudle:#{poll.class}:#{poll.name}" 44 | feed.updated = File.new('data.yaml').mtime 45 | feed.authors << Atom::Person.new(name: 'DuD-Poll automatic notificator') 46 | feed.links << Atom::Link.new(href: $conf.siteurl + 'atom.cgi', rel: 'self') 47 | 48 | log = VCS.history 49 | log.reverse_each { |l| 50 | feed.entries << Atom::Entry.new { |e| 51 | e.title = l.comment 52 | # e.content = Atom::Content::Xhtml.new("

permalink, current version

") 53 | e.links << Atom::Link.new(href: "#{$conf.siteurl}history.cgi?revision=#{l.rev}") 54 | e.id = "urn:#{poll.class}:#{poll.name}:rev=#{l.rev}" 55 | e.updated = l.timestamp 56 | } 57 | } 58 | 59 | $cgi.out('type' => 'application/atom+xml') { feed.to_xml } 60 | -------------------------------------------------------------------------------- /authorization_required.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | require_relative 'dudle' 23 | 24 | if $cgi.include?('poll') 25 | if File.directory?($cgi['poll']) 26 | Dir.chdir($cgi['poll']) 27 | $is_poll = true 28 | 29 | # check for trailing slash 30 | if ENV.fetch('REDIRECT_URL', nil) =~ /#{$cgi['poll']}$/ 31 | $d = Dudle.new(hide_lang_chooser: true, relative_dir: "#{$cgi['poll']}/") 32 | else 33 | $d = Dudle.new(hide_lang_chooser: true) 34 | end 35 | 36 | $d << ('

' + _('Authorization required') + '

') 37 | case $cgi['user'] 38 | when 'admin' 39 | $d << _('The configuration of this poll is password-protected!') 40 | when 'participant' 41 | $d << _('This poll is password-protected!') 42 | end 43 | $d << (format(_('In order to proceed, you have to give the password for user %s.'), user: "#{$cgi['user']}")) 44 | 45 | $d.out 46 | 47 | else 48 | $cgi.out({ 'status' => 'BAD_REQUEST' }) { '' } 49 | end 50 | 51 | else 52 | $d = Dudle.new(title: _('Authorization required'), hide_lang_chooser: true) 53 | returnstr = _('Return to DuD-Poll home and schedule a new poll') 54 | authstr = _('You have to authorize yourself in order to access this page!') 55 | $d << <#{authstr}

57 | 60 |

61 | END 62 | 63 | $d.out 64 | 65 | end 66 | -------------------------------------------------------------------------------- /charset.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | $USEUTF = true 21 | $USEUTF = false if $cgi.user_agent =~ /.*MSIE [56]\..*/ 22 | $USEUTF = false if $cgi.cookies['ascii'][0] 23 | $USEUTF = true if $cgi.include?('utf') 24 | $USEUTF = false if $cgi.include?('ascii') 25 | 26 | if $USEUTF 27 | NOSORT = CGI.escapeHTML('▾▴') 28 | SORT = CGI.escapeHTML('▴') 29 | REVERSESORT = CGI.escapeHTML('▾') 30 | GODOWN = CGI.escapeHTML('⇩') 31 | GOUP = CGI.escapeHTML('⇧') 32 | 33 | YES = CGI.escapeHTML('✔') 34 | NO = CGI.escapeHTML('✘') 35 | MAYBE = CGI.escapeHTML('?') 36 | UNKNOWN = CGI.escapeHTML('–') 37 | CROSS = CGI.escapeHTML('✘') 38 | 39 | # Thanks to Antje for the symbols 40 | MONTHBACK = CGI.escapeHTML('◀') 41 | MONTHFORWARD = CGI.escapeHTML('▶') 42 | EARLIER = CGI.escapeHTML('▴') 43 | LATER = CGI.escapeHTML('▾') 44 | 45 | EDIT = CGI.escapeHTML('✎') 46 | DELETE = CGI.escapeHTML('✖') 47 | 48 | PASSWORDSTAR = CGI.escapeHTML('•') 49 | else 50 | NOSORT = '' 51 | SORT = CGI.escapeHTML('^') 52 | REVERSESORT = CGI.escapeHTML('reverse') 53 | GODOWN = CGI.escapeHTML('down') 54 | GOUP = CGI.escapeHTML('up') 55 | 56 | YES = CGI.escapeHTML('OK') 57 | NO = CGI.escapeHTML('NO') 58 | MAYBE = CGI.escapeHTML('?') 59 | UNKNOWN = CGI.escapeHTML('-') 60 | CROSS = CGI.escapeHTML('X') 61 | 62 | MONTHBACK = CGI.escapeHTML('<') 63 | MONTHFORWARD = CGI.escapeHTML('>') 64 | EARLIER = CGI.escapeHTML('') 65 | LATER = CGI.escapeHTML('') 66 | 67 | EDIT = CGI.escapeHTML('edit') 68 | DELETE = CGI.escapeHTML('delete') 69 | 70 | PASSWORDSTAR = CGI.escapeHTML('*') 71 | end 72 | 73 | UTFCHARS = CGI.escapeHTML('✔✘◀▶✍✖•▾▴') 74 | -------------------------------------------------------------------------------- /check.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | require 'pp' 24 | puts "Content-type: text/plain\n" 25 | puts 26 | 27 | def print_problems(problems) 28 | puts 'Some problem occurred:' 29 | print ' - ' 30 | puts problems.collect { |a| 31 | a.join("\n ") 32 | }.join("\n - ") 33 | end 34 | 35 | def system_info 36 | puts 'Some System Info:' 37 | puts "Ruby Version: #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" 38 | puts 'Environment:' 39 | pp ENV 40 | end 41 | problems = [] 42 | hints = [] 43 | 44 | begin 45 | hints << "You might want to config your environment within the file 'config.rb' (see 'config_sample.rb' for a starting point)" unless File.exist?('config.rb') 46 | 47 | begin 48 | require_relative 'dudle' 49 | # require "rubygems" 50 | # require "atom" FIXME: rename atom.rb 51 | rescue LoadError => e 52 | problems << ['Some Library is missing:', e.message] 53 | end 54 | 55 | unless File.exist?('locale/de/dudle.mo') 56 | problems << ['If you want a language other than English, you will need a localization and therefore need to build the .mo files. Refer the README for details.'] 57 | end 58 | 59 | if File.writable?('.') 60 | testdir = 'this-is-a-test-directory-created-by-check.cgi-it-should-be-deleted' 61 | if Dir.exist?(testdir) # might exist from a previous test 62 | require 'fileutils' 63 | FileUtils.rm_r(testdir) 64 | end 65 | Dir.mkdir(testdir) 66 | Dir.chdir(testdir) 67 | VCS.init 68 | teststring = 'This is a test' 69 | File.open('testfile', 'w') { |file| 70 | file << teststring 71 | } 72 | File.symlink('../participate.rb', 'index.cgi') 73 | VCS.add('testfile') 74 | VCS.commit('Test commit') 75 | if VCS.cat(VCS.revno, 'testfile') != teststring 76 | problems << ['git commit is not working! Please try to set the following within your .htaccess:', 'SetEnv GIT_AUTHOR_NAME="http user"', 'SetEnv GIT_AUTHOR_EMAIL=foo@example.org', 'SetEnv GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"', 'SetEnv GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"'] 77 | end 78 | Dir.chdir('..') 79 | require 'fileutils' 80 | FileUtils.rm_r(testdir) 81 | else 82 | problems << ["Your webserver needs write access to #{File.expand_path('.')}"] 83 | end 84 | rescue Exception => e 85 | if problems.empty? 86 | puts 'Some problem occurred. Please contact the developer:' 87 | pp e 88 | puts e.backtrace.join("\n") 89 | else 90 | print_problems(problems) 91 | end 92 | system_info 93 | else 94 | if problems.empty? 95 | puts 'Your environment seems to be installed correctly!' 96 | unless hints.empty? 97 | print "Some hints are following:\n - " 98 | puts hints.join("\n - ") 99 | end 100 | else 101 | print_problems(problems) 102 | system_info 103 | end 104 | end 105 | 106 | # 4. You have to build a .mo file from the .po file in order to use the 107 | # localization. Type: 108 | # make 109 | # This requires libgettext-ruby-util, potool, and make to be installed. 110 | # sudo aptitude install libgettext-ruby-util potool make 111 | 112 | end 113 | -------------------------------------------------------------------------------- /classic.css: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright 2009-2019 Benjamin Kellermann * 3 | * * 4 | * This file is part of Dudle. * 5 | * * 6 | * Dudle is free software: you can redistribute it and/or modify it under * 7 | * the terms of the GNU Affero General Public License as published by * 8 | * the Free Software Foundation, either version 3 of the License, or * 9 | * (at your option) any later version. * 10 | * * 11 | * Dudle is distributed in the hope that it will be useful, but WITHOUT ANY * 12 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or * 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * 14 | * License for more details. * 15 | * * 16 | * You should have received a copy of the GNU Affero General Public License * 17 | * along with dudle. If not, see . * 18 | ***************************************************************************/ 19 | 20 | .invisible { 21 | border: none; 22 | background-color: #FFF; 23 | } 24 | 25 | a.comment_sort, a.top_bottom_ref { 26 | font-size: small; 27 | } 28 | 29 | 30 | td.a_yes__, td.b_maybe, td.c_no___ { cursor:default; } 31 | td.a_yes__{ background-color:#0F0; } 32 | td.c_no___ { background-color:#F00; } 33 | td.b_maybe { background-color:#FF0; } 34 | td.undecided { background-color:#DDD;color:#666 } 35 | td.checkboxes { 36 | background-color: #DDD; 37 | padding-left: 0px; 38 | padding-right: 0px; 39 | } 40 | 41 | table.checkboxes{ 42 | width: 100%; 43 | border-collapse: collapse; 44 | } 45 | td.input-c_no___ { background-color: #F00 } 46 | td.input-a_yes__ { background-color: #0F0} 47 | td.input-b_maybe { background-color: #FF0} 48 | 49 | label { cursor: pointer; } 50 | 51 | td.name { 52 | text-align:right; 53 | } 54 | 55 | #active, #active input.delete { 56 | background-color:#A00; 57 | } 58 | 59 | input.delete, input.toggle{ 60 | border: none; 61 | padding: 0px; 62 | cursor: pointer; 63 | background-color: #000; 64 | color: #FFF; 65 | } 66 | 67 | td.name a,td.name a:visited { 68 | text-decoration: none; 69 | color: black; 70 | } 71 | 72 | th.weekday{ 73 | width: 2.5em; 74 | } 75 | 76 | input.chosen { background-color:#0F0; } 77 | input.navigation, input.disabled, input.chosen, input.notchosen { 78 | border-width: 1px; 79 | border-style: solid; 80 | border-color: black; 81 | padding: 0px; 82 | cursor: pointer; 83 | width: 100%; 84 | } 85 | 86 | input.navigation { 87 | color: white; 88 | background-color: black; 89 | } 90 | .navigation, .delete, .toggle{ 91 | background-color: #000; 92 | color: #FFF; 93 | } 94 | 95 | input.disabled { background-color:#DDD;color:#BBB} 96 | 97 | table { 98 | border: none; 99 | } 100 | 101 | table.calendarday { 102 | border-spacing: 2px; 103 | } 104 | 105 | table.calendarday form { 106 | padding: 0px; 107 | margin: 0px; 108 | } 109 | 110 | table.calendarday td { 111 | padding: 0px; 112 | margin: 0px; 113 | } 114 | 115 | table.calendarday td input{ 116 | margin: 0px; 117 | padding: 1px; 118 | } 119 | 120 | div.undo{ 121 | text-align: center; 122 | margin-top: 2em; 123 | } 124 | 125 | td.settingstable, table.settingstable td{ 126 | text-align: left; 127 | } 128 | table.settingstable td.label{ 129 | text-align: right; 130 | } 131 | 132 | td.historycomment{ 133 | text-align: left; 134 | } 135 | td { 136 | vertical-align:middle; 137 | text-align:center; 138 | border-width: 0px; 139 | padding-left: 3px; 140 | padding-right: 3px; 141 | } 142 | #polltable { 143 | overflow: auto; 144 | } 145 | 146 | tr.participantrow, tr#add_participant, tr#summary { 147 | background-color:Silver; 148 | } 149 | 150 | td.polls { 151 | text-align:left; 152 | } 153 | 154 | tr.participantrow:hover { 155 | background: #AAA; 156 | } 157 | 158 | th { 159 | padding-left: 3px; 160 | padding-right: 3px; 161 | color: white; 162 | background-color:black; 163 | font-weight: normal; 164 | } 165 | 166 | th a{ 167 | color: white; 168 | text-decoration: none; 169 | } 170 | 171 | td.date { 172 | color: Gray; 173 | } 174 | 175 | html { 176 | background: #CCCCD3; 177 | color: black; 178 | } 179 | #main{ 180 | margin-left: auto; 181 | margin-right:auto; 182 | } 183 | #content { 184 | background: white; 185 | padding: 2em; 186 | color: black; 187 | border: solid 1px black; 188 | top: 0px; 189 | margin-top: 0px; 190 | } 191 | 192 | .textcolumn{ 193 | text-align: justify; 194 | max-width: 45em; 195 | } 196 | 197 | .shorttextcolumn{ 198 | text-align: left; 199 | max-width: 20em; 200 | } 201 | .hint{ 202 | font-size:small; 203 | } 204 | 205 | #breadcrumbs { 206 | display: none; 207 | } 208 | 209 | div#tabs { 210 | min-width:65em; 211 | } 212 | 213 | div#tabs ul{ 214 | margin-top: 2em; 215 | } 216 | 217 | div#tabs ul, p#history{ 218 | font-size: 80%; 219 | } 220 | 221 | div#tabs ul{ 222 | margin-left: 0em; 223 | margin-bottom: 0em; 224 | padding: 0em; 225 | list-style-type: none; 226 | } 227 | 228 | div#tabs li{ 229 | background: white; 230 | border-width: 1px; 231 | display: inline; 232 | } 233 | 234 | div#tabs a{ 235 | text-decoration:none; 236 | } 237 | 238 | li.active_tab{ 239 | border-style: solid solid none; 240 | padding-bottom: 1px; 241 | } 242 | 243 | li.nonactive_tab{ 244 | border-style: solid; 245 | } 246 | 247 | li.nonactive_tab a:hover { 248 | background: #CCC; 249 | } 250 | 251 | li.separator_tab { 252 | margin-left: 2em; 253 | } 254 | 255 | pre#configwarning { 256 | font-family: "Courier New",Courier,monospace; 257 | letter-spacing:0; 258 | margin-top: -12ex; 259 | line-height:95%; 260 | margin-left: -2ex; 261 | } 262 | .warning, .error{ 263 | color: red; 264 | } 265 | 266 | h1 { 267 | text-align: center; 268 | } 269 | 270 | div.comment { 271 | margin-top: 1ex; 272 | line-height: 1.4em; 273 | } 274 | 275 | form#ac_participant, form#ac { 276 | background: #EEE; 277 | } 278 | form#ac_participant, form#ac, form#ac_admin{ 279 | padding: 1em; 280 | } 281 | 282 | #languageChooser { 283 | margin-top: 1em; 284 | text-align: center; 285 | } 286 | 287 | div#languageChooser li { 288 | list-style:none; 289 | display: inline; 290 | } 291 | -------------------------------------------------------------------------------- /config_defaults.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | require 'ostruct' 21 | $conf = OpenStruct.new 22 | 23 | $conf.vcs = 'git' 24 | 25 | case $cgi.server_port 26 | when 80 27 | protocol = 'http' 28 | port = '' 29 | when 443 30 | protocol = 'https' 31 | port = '' 32 | else 33 | protocol = 'http' 34 | port = ":#{$cgi.server_port}" 35 | end 36 | $conf.siteurl = "#{protocol}://#{$cgi.server_name}#{port}#{$cgi.script_name.gsub(%r{[^/]*$}, '')}" 37 | 38 | $conf.random_chars = 7 39 | 40 | $conf.breadcrumbs = [] 41 | $conf.header = [] 42 | $conf.footer = [] 43 | 6.times { |i| 44 | $conf.header << "
" 45 | $conf.footer << "
" 46 | } 47 | 48 | $conf.errorlog = '' 49 | $conf.bugreportmail = "webmaster@#{$cgi.server_name}" 50 | $conf.auto_send_report = false 51 | $conf.known_errors = [] 52 | 53 | $conf.indexnotice = '' 54 | 55 | $conf.examples = [] 56 | 57 | $conf.examplenotice = '' 58 | 59 | $conf.aboutnotice = '' 60 | 61 | $conf.default_css = 'default.css' 62 | 63 | $conf.dudle_src = 'https://github.com/kellerben/dudle/' 64 | 65 | $conf.bots = /bot/i 66 | 67 | if File.exist?('config.rb') || File.exist?('../config.rb') 68 | require_relative 'config' 69 | end 70 | 71 | require_relative "vcs_#{$conf.vcs}" 72 | -------------------------------------------------------------------------------- /config_sample.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | # The license terms (AGPL) demands you to publish your sourcecode if you made 21 | # any modifications to the server side code. The following URL will be visible 22 | # at the about page. 23 | # $conf.dudle_src = "https://github.com/kellerben/dudle/" 24 | 25 | # Only git is supported currently 26 | # $conf.vcs = "git" 27 | 28 | # Change only if the url is not determined correctly (e.g. at the start page) 29 | # the default is: 30 | # $conf.siteurl = "#{protocol}://#{$cgi.server_name}#{port}#{$cgi.script_name.gsub(/[^\/]*$/,"")}" 31 | # you may adjust it and replace the magic guessing with something like: 32 | # $conf.siteurl = "https://dudle.example.org#{$cgi.script_name.gsub(/[^\/]*$/,"")}" 33 | # or even 34 | # $conf.siteurl = "http://example.org:9999/exampledir" 35 | 36 | # You may insert some sites, which are under your site 37 | # A breadcrumb will be generated in the way: 38 | # TUD -> ... -> Fakultät Informatik -> Professur DuD -> dudle -> poll 39 | # $conf.breadcrumbs = [ 40 | # "TUD", 41 | # "...", 42 | # "Fakultät Informatik", 43 | # "Professur Datenschutz und Datensicherheit" 44 | # ] 45 | 46 | # If you want to encourage the user to send bug reports, state the errorlog, 47 | # which you have configured in your apache conf with the ErrorLog directive. 48 | # In addition, you can change the email address to yours, if you want to 49 | # receive the mails instead of me (the developer). 50 | # You would do me a favor, if you configure this with my address, however, 51 | # if you do not want people to read parts of your error log, leave the 52 | # $conf.errorlog unset! 53 | # Make sure, that your apache can read this file 54 | # (which usually is not the case for /var/log/apache2/*) 55 | # You have 2 Options: 56 | # 1. change logrotate to allow /var/log/apache2/* to be read by apache 57 | # (=> change the line »create 640 root adm«) 58 | # 2. change $conf.errorlog to another file and create a new rule for logrotate. 59 | # DO NOT FORGET TO ADD THE ERROR LOG TO LOGROTATE IF YOU CHANGE THE PATH 60 | # TO OTHER THAN /var/log/apache2/* ! 61 | # If you do not know what to do what I am speaking about, just do not uncomment 62 | # the next line 63 | # $conf.errorlog = "/var/log/dudle_error.log" 64 | # $conf.bugreportmail = "webmaster@yoursite.example.org" 65 | 66 | # Send bug reports automatically with the program “mail” 67 | # $conf.auto_send_report = false 68 | 69 | # Add the following htmlcode to the startpage. 70 | # Example: displays all available Polls 71 | # $conf.indexnotice = <Available Polls 73 | # 74 | # 75 | # 76 | # 77 | # INDEXNOTICE 78 | # Dir.glob("*/data.yaml").sort_by{|f| 79 | # File.new(f).mtime 80 | # }.reverse.collect{|f| f.gsub(/\/data\.yaml$/,'') }.each{|site| 81 | # $conf.indexnotice += < 83 | # 84 | # 85 | # 86 | # INDEXNOTICE 87 | # } 88 | # $conf.indexnotice += "
PollLast change
#{CGI.escapeHTML(site)}#{File.new(site + "/data.yaml").mtime.strftime('%d.%m, %H:%M')}
" 89 | 90 | # Add some Example Polls to the example page. 91 | # You may create those using the normal interface 92 | # and make them password protected afterwards 93 | # .htaccess and .htdigest are deleted after 94 | # example creation (defining password protected 95 | # examples is not possible therefore) 96 | # $conf.examples = [ 97 | # { 98 | # :url => "coffeebreak", 99 | # :description => _("Event-scheduling poll"), 100 | # :new_environment => true, 101 | # },{ 102 | # :url => "coffee", 103 | # :description => _("Normal poll"), 104 | # :revno => 34 105 | # },{ 106 | # :url => "Cheater", 107 | # :description => "Cheater", 108 | # :hidden => true 109 | # } 110 | # ] 111 | 112 | # Add the following htmlcode to the example page. 113 | # $conf.examplenotice = <Screencasts 115 | #
    116 | #
  1. Register a new user
  2. 117 | #
  3. Setup a new poll
  4. 118 | #
  5. Participate in a poll
  6. 119 | #
120 | # EXAMPLENOTICE 121 | 122 | # Add the following htmlcode to the about page. 123 | # $conf.aboutnotice = < 125 | #

Bugs/Features

126 | # 130 | # 131 | # ABOUTNOTICE 132 | 133 | # choose a default stylesheet 134 | # e.g., "classic.css", "css/foobar.css", ... 135 | # $conf.default_css = "default.css" 136 | -------------------------------------------------------------------------------- /customize.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | $:.push('..') 25 | require_relative 'dudle' 26 | 27 | $d = Dudle.new 28 | 29 | $d << ('

' + _('Customize personal settings') + '

') 30 | $d << ('

' + _("You need cookies enabled in order to personalize your settings.") + '

') 31 | 32 | def choosetable(options, cursetting) 33 | ret = < 35 | 36 | HEAD 37 | ret += '' + _('Current setting') + '' 38 | ret += '' + _('Description') + '' 39 | ret += '' 40 | options.each { |description, href, title| 41 | selected = href == cursetting 42 | ret += '' 43 | ret += YES if selected 44 | ret += "" 45 | ret += "" unless selected 46 | ret += description 47 | ret += '' unless selected 48 | ret += '' 49 | } 50 | ret += '' 51 | ret 52 | end 53 | 54 | a = [ 55 | [_('Use special characters') + " (#{UTFCHARS})", 'utf', _('Use this option if you see the characters in the parenthesis.')], 56 | [_('Use only normal strings'), 'ascii', _('Use this option if you have problems with some characters.')] 57 | ] 58 | $d.html.add_cookie('ascii', 'true', '/', Time.now + (1 * 60 * 60 * 24 * 365 * ($USEUTF ? -1 : 1))) 59 | $d << "
" 60 | $d << ('

' + _('Charset') + '

') 61 | $d << choosetable(a, $USEUTF ? 'utf' : 'ascii') 62 | $d << '
' 63 | 64 | $d << "
" 65 | $d << ('

' + _('Stylesheet') + '

') 66 | $d << choosetable($d.css.collect { |href| [href.scan(%r{([^/]*)\.css}).flatten[0], "css=#{href}"] }, "css=#{$d.user_css}") 67 | $d << '
' 68 | 69 | username = $cgi.cookies['username'][0] 70 | if $cgi.include?('delete_username') 71 | $d.html.add_cookie('username', '', '/', Time.now - (1 * 60 * 60 * 24 * 365)) 72 | username = '' 73 | elsif $cgi.include?('username') && $cgi['username'] != '' 74 | $d.html.add_cookie('username', $cgi['username'], '/', Time.now + (1 * 60 * 60 * 24 * 365)) 75 | end 76 | 77 | defaultuserstr = _('Default Username') 78 | usernamestr = _('Username:') 79 | $d << < 81 |

#{defaultuserstr}

82 | 83 | 84 | 85 | 88 | 97 | 98 | 99 | 100 | 107 | 108 | 109 | 110 | 120 |
86 | 87 | 89 | CHARSET 90 | 91 | if username && !$cgi.include?('edit') 92 | $d << <#{CGI.escapeHTML(username)} 94 | 95 | 96 |
101 | CHARSET 102 | $d << ("") 103 | else 104 | $d << < 106 |
111 | CHARSET 112 | $d << ("") 113 | end 114 | 115 | $d.html << ("") if username 116 | 117 | $d << < 119 |
121 | 122 | 123 | CHARSET 124 | 125 | $d.out 126 | end 127 | -------------------------------------------------------------------------------- /customize.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | $:.push('..') 25 | require_relative 'dudle' 26 | 27 | $d = Dudle.new 28 | 29 | $d << ('

' + _('Customize personal settings') + '

') 30 | $d << _("You need cookies enabled in order to personalize your settings.") 31 | 32 | def choosetable(options, cursetting) 33 | ret = < 35 | 36 | 37 | HEAD 38 | ret += '' + _('Current setting') + '' 39 | ret += '' + _('Description') + '' 40 | ret += '' 41 | ret += '' 42 | options.each { |description, href, title| 43 | selected = href == cursetting 44 | ret += '' 45 | ret += YES if selected 46 | ret += "" 47 | ret += "" unless selected 48 | ret += description 49 | ret += '' unless selected 50 | ret += '' 51 | } 52 | ret += '' 53 | ret 54 | end 55 | 56 | a = [ 57 | [_('Use special characters') + " (#{UTFCHARS})", 'utf', _('Use this option if you see the characters in the parenthesis.')], 58 | [_('Use only normal strings'), 'ascii', _('Use this option if you have problems with some characters.')] 59 | ] 60 | $d.html.add_cookie('ascii', 'true', '/', Time.now + (1 * 60 * 60 * 24 * 365 * ($USEUTF ? -1 : 1))) 61 | $d << "
" 62 | $d << ('

' + _('Charset') + '

') 63 | $d << choosetable(a, $USEUTF ? 'utf' : 'ascii') 64 | $d << '
' 65 | 66 | $d << "
" 67 | $d << ('

' + _('Stylesheet') + '

') 68 | $d << choosetable($d.css.collect { |href| [href.scan(%r{([^/]*)\.css}).flatten[0], "css=#{href}"] }, "css=#{$d.user_css}") 69 | $d << '
' 70 | 71 | username = $cgi.cookies['username'][0] 72 | if $cgi.include?('delete_username') 73 | $d.html.add_cookie('username', '', '/', Time.now - (1 * 60 * 60 * 24 * 365)) 74 | username = '' 75 | elsif $cgi.include?('username') && $cgi['username'] != '' 76 | $d.html.add_cookie('username', $cgi['username'], '/', Time.now + (1 * 60 * 60 * 24 * 365)) 77 | end 78 | 79 | defaultuserstr = _('Default Username') 80 | usernamestr = _('Username:') 81 | $d << < 83 |

#{defaultuserstr}

84 |
85 | 86 | 87 | 90 | 99 | 100 | 101 | 102 | 109 | 110 | 111 | 112 | 122 |
88 | 89 | 91 | CHARSET 92 | 93 | if username && !$cgi.include?('edit') 94 | $d << <#{CGI.escapeHTML(username)} 96 | 97 | 98 |
103 | CHARSET 104 | $d << ("") 105 | else 106 | $d << < 108 |
113 | CHARSET 114 | $d << ("") 115 | end 116 | 117 | $d.html << ("") if username 118 | 119 | $d << < 121 |
123 |
124 | 125 | CHARSET 126 | 127 | $d.out 128 | end 129 | -------------------------------------------------------------------------------- /date_locale.rb: -------------------------------------------------------------------------------- 1 | # Make a 'localization' for Date, DateTime and Time. 2 | # 3 | # This is not using locale, but if you use locale, it is detected and locale sensitive. 4 | # 5 | # The output is in iso-8859-1, other encodings can be set with Date_locale.set_target_encoding. 6 | # 7 | 8 | require 'date' 9 | 10 | # 11 | # Adaption for a localized Date-class 12 | # 13 | # Solution based on discussion at ruby-forum.de 14 | # -http://forum.ruby-portal.de/viewtopic.php?f=1&t=10527&start=0 15 | # 16 | module Date_locale 17 | # Constant/Hash with the supported languages. 18 | # 19 | # Initial definitions are taken from localization_simplified. 20 | # 21 | # Changes: 22 | # * added de_at 23 | # * adapted :pt to pt_br (original :pt was French). 24 | DATE_TEXTS = { 25 | ca: { 26 | monthnames: [nil] + %w[gener febrer març abril maig juny juliol agost setembre octubre novembre desembre], 27 | abbr_monthnames: [nil] + %w[gen feb mar abr mai jun jul ago set oct nov des], 28 | daynames: %w[diumenge dilluns dimarts dimecres dijous divendres dissabte], 29 | abbr_daynames: %w[dg dl dt dc dj dv ds] 30 | }, 31 | cf: { 32 | monthnames: [nil] + %w[Janvier Février Mars Avril Mai Juin Juillet Août Septembre Octobre Novembre Décembre], 33 | abbr_monthnames: [nil] + %w[Jan Fev Mar Avr Mai Jun Jui Aou Sep Oct Nov Dec], 34 | daynames: %w[Dimanche Lundi Mardi Mercredi Jeudi Vendredi Samedi], 35 | abbr_daynames: %w[Dim Lun Mar Mer Jeu Ven Sam] 36 | }, 37 | cs: { 38 | monthnames: [nil] + %w[Leden Únor Březen Duben Květen Červen Červenec Srpen Září Říjen Listopad Prosinec], 39 | abbr_monthnames: [nil] + %w[Led Úno Bře Dub Kvě Čvn Čvc Srp Zář Říj Lis Pro], 40 | daynames: %w[Neděle Pondělí Úterý Středa Čtvrtek Pátek Sobota], 41 | abbr_daynames: %w[Ne Po Út St Čt Pá So] 42 | }, 43 | da: { 44 | monthnames: [nil] + %w[januar februar marts april maj juni juli august september oktober november december], 45 | abbr_monthnames: [nil] + %w[jan feb mar apr maj jun jul aug sep okt nov dec], 46 | daynames: %w[søndag mandag tirsdag onsdag torsdag fredag lørdag], 47 | abbr_daynames: %w[søn man tir ons tors fre lør] 48 | }, 49 | de: { 50 | monthnames: [nil] + %w[Januar Februar März April Mai Juni Juli August September Oktober November Dezember], 51 | abbr_monthnames: [nil] + %w[Jan Feb Mrz Apr Mai Jun Jul Aug Sep Okt Nov Dez], 52 | daynames: %w[Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag], 53 | abbr_daynames: %w[So Mo Di Mi Do Fr Sa] 54 | }, 55 | de_at: { 56 | monthnames: [nil] + %w[Jänner Feber März April Mai Juni Juli August September Oktober November Dezember], 57 | abbr_monthnames: [nil] + %w[Jan Feb Mrz Apr Mai Jun Jul Aug Sep Okt Nov Dez], 58 | daynames: %w[Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag], 59 | abbr_daynames: %w[So Mo Di Mi Do Fr Sa] 60 | }, 61 | et: { 62 | monthnames: [nil] + %w[Jaanuar Veebruar Märts Aprill Mai Juuni Juuli August September Oktober November Detsember], 63 | abbr_monthnames: [nil] + %w[Jaan. Veebr. Märts Apr. Mai Juuni Juuli. Aug. Sep. Okt. Nov. Dets.], 64 | daynames: %w[Pühapäev Esmapäev Teisipäev Kolmapäev Neljapäev Reedel Laupäev], 65 | abbr_daynames: %w[P E T K N R L] 66 | }, 67 | en: { 68 | monthnames: [nil] + %w[January February March April May June July August September October November December], 69 | abbr_monthnames: [nil] + %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec], 70 | daynames: %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday], 71 | abbr_daynames: %w[Sun Mon Tue Wed Thu Fri Sat] 72 | }, 73 | es: { 74 | monthnames: [nil] + %w[enero febrero marzo abril mayo junio julio agosto septiembre octubre noviembre diciembre], 75 | abbr_monthnames: [nil] + %w[ene feb mar abr may jun jul ago sep oct nov dic], 76 | daynames: %w[domingo lunes martes miércoles jueves viernes sábado], 77 | abbr_daynames: %w[dom lun mar mié jue vie sáb] 78 | }, 79 | es_ar: { 80 | monthnames: [nil] + %w[enero febrero marzo abril mayo junio julio agosto septiembre octubre noviembre diciembre], 81 | abbr_monthnames: [nil] + %w[ene feb mar abr may jun jul ago sep oct nov dic], 82 | daynames: %w[domingo lunes martes miércoles jueves viernes sábado], 83 | abbr_daynames: %w[dom lun mar mié jue vie sáb] 84 | }, 85 | fi: { 86 | monthnames: [nil] + %w[tammikuu helmikuu maaliskuu huhtikuu toukokuu kesäkuu heinäkuu elokuu syyskuu lokakuu marraskuu joulukuu], 87 | abbr_monthnames: [nil] + %w[tammi helmi maalis huhti touko kesä heinä elo syys loka marras joulu], 88 | daynames: %w[sunnuntai maanantai tiistai keskiviikko torstai perjantai lauantai], 89 | abbr_daynames: %w[su ma ti ke to pe la] 90 | }, 91 | fr: { 92 | monthnames: [nil] + %w[janvier février mars avril mai juin juillet août septembre octobre novembre decembre], 93 | abbr_monthnames: [nil] + %w[jan fév mar avr mai jun jul aoû sep oct nov déc], 94 | daynames: %w[dimanche lundi mardi mercredi jeudi vendredi samedi], 95 | abbr_daynames: %w[dim lun mar mer jeu ven sam] 96 | }, 97 | it: { 98 | monthnames: [nil] + %w[Gennaio Febbraio Marzo Aprile Maggio Giugno Luglio Agosto Settembre Ottobre Novembre Dicembre], 99 | daynames: %w[Domenica Lunedì Martedì Mercoledì Giovedì Venerdì Sabato], 100 | abbr_monthnames: [nil] + %w[Gen Feb Mar Apr Mag Giu Lug Ago Set Ott Nov Dic], 101 | abbr_daynames: %w[Dom Lun Mar Mer Gio Ven Sab] 102 | }, 103 | ko: { 104 | monthnames: [nil] + %w[1월 2월 3월 4월 5월 6월 7월 8월 9월 10월 11월 12월], 105 | abbr_monthnames: [nil] + %w[1 2 3 4 5 6 7 8 9 10 11 12], 106 | daynames: %w[일요일 월요일 화요일 수요일 목요일 금요일 토요일], 107 | abbr_daynames: %w[일 월 화 수 목 금 토] 108 | }, 109 | nl: { 110 | monthnames: [nil] + %w[Januari Februari Maart April Mei Juni Juli Augustus September Oktober November December], 111 | abbr_monthnames: [nil] + %w[Jan Feb Maa Apr Mei Jun Jul Aug Sep Okt Nov Dec], 112 | daynames: %w[Zondag Maandag Dinsdag Woensdag Donderdag Vrijdag Zaterdag], 113 | abbr_daynames: %w[Zo Ma Di Wo Do Vr Za] 114 | }, 115 | no: { 116 | monthnames: [nil] + %w[januar februar mars april mai juni juli august september oktober november desember], 117 | abbr_monthnames: [nil] + %w[jan feb mar apr mai jun jul aug sep okt nov des], 118 | daynames: %w[søndag mandag tirsdag onsdag torsdag fredag lørdag], 119 | abbr_daynames: %w[søn man tir ons tors fre lør] 120 | }, 121 | pt: { 122 | monthnames: [nil] + %w[Janeiro Fevereiro Março Abril Maio Junho Julho Agosto Setembro Outubro Novembro Dezembro], 123 | abbr_monthnames: [nil] + %w[Jan Fev Mar Abr Mai Jun Jul Ago Set Out Nov Dez], 124 | daynames: %w[domingo segunda terça quarta quinta sexta sábado], 125 | abbr_daynames: %w[Dom Seg Ter Qua Qui Sex Sab] 126 | }, 127 | pt_br: { 128 | monthnames: [nil] + %w[janeiro fevereiro março abril maio junho julho agosto setembro outubro novembro dezembro], 129 | abbr_monthnames: [nil] + %w[jan fev mar abr mai jun jul ago set out nov dez], 130 | daynames: %w[domingo segunda terça quarta quinta sexta sábado], 131 | abbr_daynames: %w[dom seg ter qua qui sex sáb] 132 | }, 133 | ru: { 134 | monthnames: [nil] + %w[Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь], 135 | abbr_monthnames: [nil] + %w[Янв Фев Мар Апр Май Июн Июл Авг Сен Окт Ноя Дек], 136 | daynames: %w[Воскресенье Понедельник Вторник Среда Четверг Пятница Суббота], 137 | abbr_daynames: %w[Вск Пнд Втр Сре Чет Пят Суб] 138 | }, 139 | sv: { 140 | monthnames: [nil] + %w[januari februari mars april maj juni juli augusti september oktober november december], 141 | abbr_monthnames: [nil] + %w[jan feb mar apr maj jun jul aug sep okt nov dec], 142 | daynames: %w[söndag måndag tisdag onsdag torsdag fredag lördag], 143 | abbr_daynames: %w[sön mån tis ons tors fre lör] 144 | }, 145 | sr: { 146 | monthnames: [nil] + %w[Januar Februar Mart April Maj Jun Jul Avgust Septembar Oktobar Novembar Decembar], 147 | abbr_monthnames: [nil] + %w[Jan Feb Mar Apr Maj Jun Jul Aug Sep Okt Nov Dec], 148 | daynames: %w[Nedelja Ponedeljak Utorak Sreda Četvrtak Petak Subota], 149 | abbr_daynames: %w[Ned Pon Uto Sre Čet Pet Sub] 150 | }, 151 | hu: { 152 | monthnames: [nil] + %w[január február március május június július augusztus szeptember október november december], 153 | abbr_monthnames: [nil] + %w[jan feb már ápr máj jún júl aug sze okt nov dec], 154 | daynames: %w[vasárnap hétfő kedd szerda csütörtök péntek szombat], 155 | abbr_daynames: %w[vas hét ked sze csü pén szo] 156 | } 157 | } 158 | # ~ puts DATE_TEXTS.to_yaml 159 | 160 | # Not really necessary. 161 | # But I want to avoid later changes. 162 | DATE_TEXTS.freeze 163 | 164 | # 165 | # Test if the seleted language is available in Date_locale. 166 | def self.locale?(lang) 167 | DATE_TEXTS[lang] 168 | end 169 | 170 | # 171 | # Get the key for the wanted language. 172 | # 173 | # Allows the usage (or not to use) locale. 174 | def self.get_language_key(lang = nil) 175 | # 176 | # What's the better solution? Check for locale, or check for the method :language? 177 | # 178 | # if defined?( Locale ) and lang.is_a?(Locale::TagList) 179 | if lang.respond_to?(:language) 180 | if lang.respond_to?(:charset) && !lang.charset.nil? 181 | Date_locale.set_target_encoding(lang.charset) 182 | end 183 | return lang.language.to_sym 184 | end 185 | 186 | case lang 187 | when nil # Undefined default, take actual locale or en 188 | defined?(Locale) ? Locale.current.language.to_sym : :en 189 | # This code require locale (or you get an error "uninitialized constant Date_locale::Locale") 190 | # when Locale::Object 191 | # return lang.language.to_sym 192 | else 193 | lang.to_sym 194 | end 195 | end 196 | 197 | # 198 | # strftime with the day- and month names in the selected language. 199 | # 200 | # Lang can be a language symbol or a locale. 201 | def strftime_locale(format = '%F', lang = nil) 202 | lang = Date_locale.get_language_key(lang) 203 | 204 | # Get the texts 205 | if DATE_TEXTS[lang] 206 | daynames = DATE_TEXTS[lang][:daynames] 207 | abbr_daynames = DATE_TEXTS[lang][:abbr_daynames] 208 | monthnames = DATE_TEXTS[lang][:monthnames] 209 | abbr_monthnames = DATE_TEXTS[lang][:abbr_monthnames] 210 | else 211 | # raise "Missing Support for locale #{lang.inspect}" 212 | # fallback to english 213 | daynames = DATE_TEXTS[:en][:daynames] 214 | abbr_daynames = DATE_TEXTS[:en][:abbr_daynames] 215 | monthnames = DATE_TEXTS[:en][:monthnames] 216 | abbr_monthnames = DATE_TEXTS[:en][:abbr_monthnames] 217 | end 218 | 219 | # Make the original replacements, after.... 220 | result = strftime_orig( 221 | # ...you replaced the language dependent parts. 222 | format.gsub(/%([aAbB])/) { |_m| 223 | case ::Regexp.last_match(1) 224 | when 'a' then abbr_daynames[wday] 225 | when 'A' then daynames[wday] 226 | when 'b' then abbr_monthnames[mon] 227 | when 'B' then monthnames[mon] 228 | else 229 | raise 'Date#strftime: InputError' 230 | end 231 | } 232 | ) 233 | result.encode('utf-8') 234 | end # strftime_locale(format = '%F', lang = :en ) 235 | end # module Date_locale 236 | 237 | class Date 238 | include Date_locale 239 | alias strftime_orig strftime 240 | 241 | # Redefine strftime with flexible daynames. 242 | # 243 | def strftime(format = '%F', lang = nil) 244 | strftime_locale(format, lang) 245 | end # strftime 246 | end # class Date 247 | 248 | # 249 | # Redefine strftime for DateTime 250 | # 251 | class DateTime 252 | # No alias! It is done already in class Date. 253 | # alias :strftime_orig_date :strftime 254 | 255 | # Redefine strftime. 256 | # strftime_orig is already defined in Date. 257 | def strftime(format = '%F', lang = nil) 258 | strftime_locale(format, lang) 259 | end # strftime 260 | end 261 | 262 | class Time 263 | include Date_locale 264 | alias strftime_orig strftime 265 | # Redefine strftime for locale versions. 266 | def strftime(format = '%F', lang = nil) 267 | strftime_locale(format, lang) 268 | end # strftime 269 | end 270 | 271 | # 272 | # Make some quick tests 273 | # 274 | if __FILE__ == $0 275 | # ~ require 'date_locale' 276 | 277 | d = Date.new(2009, 10, 21) 278 | puts d.strftime('de: %A {%a} {%A} {%W} %w ', :de) #=> de: Mittwoch {Mi} {Mittwoch} {42} 3 (loc: en) 279 | puts d.strftime('en: %A {%a} {%A} {%W} %w ', :en) #=> en: Wednesday {Wed} {Wednesday} {42} 3 (loc: en) 280 | 281 | puts '=======Load locale' 282 | require 'locale' 283 | Locale.current = 'de' 284 | puts d.strftime("#{Locale.current}: %A {%a} {%A} {%W} %w") #=> de: Mittwoch {Mi} {Mittwoch} {42} 3 285 | Locale.current = 'en' 286 | puts d.strftime("#{Locale.current}: %A {%a} {%A} {%W} %w") #=> en: Wednesday {Wed} {Wednesday} {42} 3 287 | puts d.strftime("de: %A {%a} {%A} {%W} %w (loc: #{Locale.current})", :de) #=> de: Mittwoch {Mi} {Mittwoch} {42} 3 (loc: en) 288 | puts d.strftime("en: %A {%a} {%A} {%W} %w (loc: #{Locale.current})", :en) #=> en: Wednesday {Wed} {Wednesday} {42} 3 (loc: en) 289 | end # if __FILE__ == $0 290 | -------------------------------------------------------------------------------- /default.css: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright 2009-2019 Benjamin Kellermann * 3 | * 2010 Stefanie Pötzsch * 4 | * * 5 | * This file is part of Dudle. * 6 | * * 7 | * Dudle is free software: you can redistribute it and/or modify it under * 8 | * the terms of the GNU Affero General Public License as published by * 9 | * the Free Software Foundation, either version 3 of the License, or * 10 | * (at your option) any later version. * 11 | * * 12 | * Dudle is distributed in the hope that it will be useful, but WITHOUT ANY * 13 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or * 14 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * 15 | * License for more details. * 16 | * * 17 | * You should have received a copy of the GNU Affero General Public License * 18 | * along with dudle. If not, see . * 19 | ***************************************************************************/ 20 | 21 | .invisible { 22 | border: none; 23 | background-color: #FFF; 24 | } 25 | 26 | a, a:visited { 27 | color:#36c; 28 | text-decoration:none; 29 | } 30 | 31 | a:hover, a:active { 32 | color:#36c; 33 | text-decoration:underline; 34 | } 35 | a.comment_sort, a.top_bottom_ref { 36 | font-size: small; 37 | } 38 | 39 | td.a_yes__, td.b_maybe, td.c_no___ { cursor:default;} 40 | td.a_yes__, tr.input-a_yes__ td, input.chosen { background-color:#9C6} 41 | input.chosen:hover { background-color: #8B5 } 42 | td.c_no___, tr.input-c_no___ td { background-color:#F96 } 43 | td.b_maybe, tr.input-b_maybe td { background-color:#FF6} 44 | td.undecided { background-color:#DDD } 45 | 46 | td.checkboxes { 47 | background-color: #FFF; 48 | padding-left: 0px; 49 | padding-right: 0px; 50 | } 51 | 52 | table.checkboxes{ 53 | width: 100%; 54 | border-collapse: collapse; 55 | border:none; 56 | } 57 | 58 | label { cursor: pointer; } 59 | 60 | td.name { 61 | text-align:right; 62 | } 63 | 64 | #active, #active input.delete{ 65 | background-color:#CCF; 66 | } 67 | 68 | input.delete, input.toggle{ 69 | border: none; 70 | cursor: pointer; 71 | } 72 | 73 | input.navigation, input.disabled, input.chosen, input.notchosen { 74 | border-width: 1px; 75 | border-style: solid; 76 | border-color: black; 77 | padding: 0px; 78 | cursor: pointer; 79 | width: 100%; 80 | } 81 | 82 | input.navigation { 83 | border: none; 84 | } 85 | .navigation, .notchosen, .disabled, .delete, .toggle{ 86 | background-color: #EEE; 87 | } 88 | 89 | a.editcolumn { 90 | color: black; 91 | } 92 | a.editcolumn:hover { 93 | text-decoration:none; 94 | } 95 | .navigation:hover, .notchosen:hover, .disabled:hover, .editcolumn:hover, #active .delete:hover, .delete:hover, .toggle:hover { 96 | background-color: #CCC; 97 | } 98 | 99 | input.disabled { 100 | color:#AAA 101 | } 102 | 103 | table { 104 | border: none; 105 | } 106 | 107 | table.calendarday { 108 | border-spacing: 2px; 109 | } 110 | 111 | div#AddRemoveColumndaysDescription{ 112 | max-width: 15em; 113 | } 114 | 115 | table.calendarday th, table.calendarday td{ 116 | min-width: 2em; 117 | } 118 | 119 | table.calendarday td.navigation{ 120 | min-width: 0em; 121 | } 122 | 123 | table.calendarday form { 124 | padding: 0px; 125 | margin: 0px; 126 | } 127 | 128 | table.calendarday td { 129 | padding: 0px; 130 | margin: 0px; 131 | } 132 | 133 | table.calendarday td.navigation{ 134 | padding-left: 0.5ex; 135 | padding-right: 0.5ex; 136 | } 137 | 138 | table.calendarday td input{ 139 | margin: 0px; 140 | padding: 1px; 141 | } 142 | 143 | table.calendarday th input.delete { 144 | padding: 0px; 145 | } 146 | table.calendarday td input.toggle { 147 | padding:0 0.5em 0 0.5em; 148 | } 149 | 150 | div.undo{ 151 | text-align: center; 152 | margin-top: 2em; 153 | } 154 | 155 | td.settingstable, table.settingstable td{ 156 | text-align: left; 157 | } 158 | table.settingstable td.label{ 159 | text-align: right; 160 | } 161 | td.separator_top{ 162 | border-top:solid thin; 163 | padding-top:0.7ex; 164 | } 165 | td.separator_bottom{ 166 | padding-bottom:0.7ex; 167 | } 168 | tr#usernamesetting { 169 | height: 1.7em 170 | } 171 | 172 | td.historycomment{ 173 | text-align: left; 174 | } 175 | td { 176 | vertical-align:middle; 177 | text-align:center; 178 | border-width: 0px; 179 | padding-left: 3px; 180 | padding-right: 3px; 181 | padding-bottom: 1px; 182 | padding-top: 1px; 183 | } 184 | 185 | #polltable { 186 | overflow: auto; 187 | } 188 | tr#add_participant{ 189 | background: #EEE; 190 | } 191 | 192 | tr.participantrow:hover { 193 | background: #EEE; 194 | } 195 | 196 | tr.participantrow{ 197 | background-color:#fff; 198 | } 199 | 200 | tr#summary { 201 | background-color:#eee; 202 | } 203 | td.sum, tr#summary { 204 | margin-top:5ex; 205 | } 206 | td.sum{ color: #FFF} 207 | td.match_100{background-color: #000000; } 208 | td.match_90{ background-color: #111111; } 209 | td.match_80{ background-color: #222222; } 210 | td.match_70{ background-color: #333333; } 211 | td.match_60{ background-color: #444444; } 212 | td.match_50{ background-color: #555555; } 213 | td.match_40{ background-color: #666666; } 214 | td.match_30{ background-color: #777777; } 215 | td.match_20{ background-color: #888888; } 216 | td.match_10{ background-color: #999999; } 217 | td.match_0{ background-color: #AAAAAA; } 218 | 219 | 220 | td.polls { 221 | text-align:left; 222 | } 223 | 224 | th { 225 | font-weight: normal; 226 | padding-left: 3px; 227 | padding-right: 3px; 228 | background-color: #EEE; 229 | border:none; 230 | } 231 | 232 | td.date { 233 | color:#ccc; 234 | text-align: left; 235 | } 236 | 237 | html { 238 | background: #EEE; 239 | font-family: Arial, Helvetica, sans-serif, serif, monospace, cursive, fantasy; 240 | } 241 | #main{ 242 | margin-left: auto; 243 | margin-right:auto; 244 | } 245 | #content { 246 | background: #fff; 247 | padding: 2em; 248 | color: #000; 249 | border: solid 1px #000; 250 | border-bottom: none; 251 | top: 0px; 252 | margin-top: 0px; 253 | } 254 | #wizzard_navigation{ 255 | margin-left: 0; 256 | padding-left: 0; 257 | margin-top: 2ex; 258 | } 259 | #wizzard_navigation table, #wizzard_navigation table td{ 260 | padding-left:0; 261 | border-spacing:0; 262 | } 263 | 264 | .textcolumn, .shorttextcolumn{ 265 | line-height: 1.4em; 266 | } 267 | 268 | .textcolumn{ 269 | text-align: justify; 270 | max-width: 45em; 271 | } 272 | 273 | .shorttextcolumn{ 274 | text-align: left; 275 | max-width: 20em; 276 | } 277 | .hint{ 278 | font-size: small; 279 | line-height: 1.4em; 280 | } 281 | 282 | #breadcrumbs { 283 | display: none; 284 | } 285 | 286 | div#tabs{ 287 | font-size:80%; 288 | min-width:65em; 289 | } 290 | 291 | div#tabs ul{ 292 | margin-top: 2em; 293 | } 294 | 295 | div#tabs ul{ 296 | margin-left: 0em; 297 | margin-bottom: 0em; 298 | padding: 0em; 299 | list-style: none; 300 | } 301 | 302 | div#tabs li{ 303 | background: white; 304 | border-width: 1px; 305 | display: inline; 306 | } 307 | 308 | div#tabs a{ 309 | text-decoration:none; 310 | } 311 | 312 | li.active_tab{ 313 | border-style: solid solid none; 314 | padding-bottom: 1px; 315 | } 316 | 317 | li.nonactive_tab{ 318 | border-style: solid; 319 | border-color:#CCC; 320 | border-bottom:none; 321 | } 322 | 323 | li.nonactive_tab a:hover { 324 | background: #EEE; 325 | } 326 | 327 | li.separator_tab { 328 | margin-left: 2em; 329 | } 330 | 331 | pre#configwarning { 332 | font-family: "Courier New",Courier,monospace; 333 | letter-spacing:0; 334 | margin-top: -12ex; 335 | line-height:95%; 336 | margin-left: -2ex; 337 | } 338 | 339 | .warning, .error{ 340 | color: red; 341 | } 342 | 343 | h1{ 344 | text-align:center; 345 | } 346 | 347 | h3{ 348 | margin-bottom: 0.1em; 349 | } 350 | 351 | .comment { 352 | margin-top: 1.5em; 353 | } 354 | 355 | 356 | form#ac_participant, form#ac { 357 | background: #EEE; 358 | } 359 | form#ac_participant, form#ac, form#ac_admin{ 360 | padding: 1em; 361 | } 362 | 363 | div#languageChooser{ 364 | background: #fff; 365 | padding: 2em; 366 | color: #000; 367 | border: solid 1px #000; 368 | border-top: none; 369 | text-align: center; 370 | } 371 | 372 | div#languageChooser li { 373 | list-style:none; 374 | display: inline; 375 | } 376 | 377 | input#add_participant_input { 378 | width: 100%; 379 | border: solid 1px; 380 | 381 | } 382 | -------------------------------------------------------------------------------- /delete_poll.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ############################################################################ 3 | # Copyright 2009-2019 Benjamin Kellermann # 4 | # # 5 | # This file is part of Dudle. # 6 | # # 7 | # Dudle is free software: you can redistribute it and/or modify it under # 8 | # the terms of the GNU Affero General Public License as published by # 9 | # the Free Software Foundation, either version 3 of the License, or # 10 | # (at your option) any later version. # 11 | # # 12 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 13 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 14 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 15 | # License for more details. # 16 | # # 17 | # You should have received a copy of the GNU Affero General Public License # 18 | # along with dudle. If not, see . # 19 | ############################################################################ 20 | 21 | if __FILE__ == $0 22 | 23 | load '../dudle.rb' 24 | $d = Dudle.new 25 | require 'fileutils' 26 | 27 | QUESTIONS = [ 28 | 'phahqu3Uib4neiRi', 29 | _('Yes, I know what I am doing!'), 30 | _('I hate these stupid entry fields.'), 31 | _('I am aware of the consequences.'), 32 | _('Please delete this poll.') 33 | ] 34 | 35 | userconfirm = CGI.escapeHTML($cgi['confirm'].strip) 36 | if $cgi.include?('confirmnumber') 37 | confirm = $cgi['confirmnumber'].to_i 38 | if userconfirm == QUESTIONS[confirm] 39 | Dir.chdir('..') 40 | 41 | if $conf.examples.collect { |e| e[:url] }.include?($d.urlsuffix) 42 | deleteconfirmstr = _('Example polls cannot be deleted.') 43 | accidentstr = _('You should never see this text.') 44 | else 45 | FileUtils.cp_r($d.urlsuffix, "/tmp/#{$d.urlsuffix}.#{rand(9_999_999)}") 46 | FileUtils.rm_r($d.urlsuffix) 47 | 48 | if $cgi.include?('return') 49 | $d.html.header['status'] = 'REDIRECT' 50 | $d.html.header['Cache-Control'] = 'no-cache' 51 | $d.html.header['Location'] = $conf.siteurl + $cgi['return'] 52 | $d.out 53 | exit 54 | end 55 | 56 | deleteconfirmstr = _('The poll was deleted successfully!') 57 | accidentstr = _('If this was done by accident, please contact the administrator of the system. The poll can be recovered for an indeterminate amount of time; it could already be too late.') 58 | end 59 | nextthingsstr = _('You can now') 60 | homepagestr = _('Return to DuD-Poll home and schedule a new poll') 61 | wikipediastr = _('Browse Wikipedia') 62 | searchstr = _('Search for something on the Internet') 63 | 64 | $d.html << %( 65 |

66 | #{deleteconfirmstr} 67 |

68 |

69 | #{accidentstr} 70 |

71 |
72 | #{nextthingsstr} 73 | 78 |
79 | ) 80 | $d.out 81 | exit 82 | else 83 | hint = %{ 84 | 85 | 86 | 91 | 92 | 93 | 94 | 99 | 100 | 101 |
87 | } 88 | hint += _('To delete the poll, you have to type:') 89 | hint += %( 90 | #{QUESTIONS[confirm]}
95 | ) 96 | hint += _('but you typed:') 97 | hint += %( 98 | #{userconfirm}
102 | ) 103 | end 104 | else 105 | confirm = rand(QUESTIONS.size - 1) + 1 106 | end 107 | 108 | $d.html << ('

' + _('Delete this poll') + '

') 109 | $d.html << ('

' + _('You want to delete the poll named') + " #{CGI.escapeHTML($d.table.name)}.
") 110 | $d.html << (_('This is an irreversible action!') + '
') 111 | $d.html << (format(_('If you are sure that you want to permanently remove this poll, please type the following sentence into the form: %s'), question: QUESTIONS[confirm])) 112 | $d.html << '

' 113 | deletestr = _('Delete') 114 | deleteform = _('Delete form') 115 | $d.html << %( 116 | #{hint} 117 |
118 | 119 | 120 | 125 |
121 | 122 | 123 | 124 |
126 |
127 | ) 128 | 129 | $d.out 130 | 131 | end 132 | -------------------------------------------------------------------------------- /dudle.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | $KCODE = 'u' if RUBY_VERSION < '1.9.0' 21 | require 'yaml' 22 | require 'cgi' 23 | 24 | $cgi ||= CGI.new 25 | 26 | require 'gettext' 27 | require 'gettext/cgi' 28 | include GetText 29 | GetText.cgi = $cgi 30 | GetText.output_charset = 'utf-8' 31 | require 'locale' 32 | 33 | if File.exist?('data.yaml') && !File.stat('data.yaml').directory? 34 | $is_poll = true 35 | GetText.bindtextdomain('dudle', path: Dir.pwd + '/../locale/') 36 | else 37 | $is_poll = false 38 | GetText.bindtextdomain('dudle', path: Dir.pwd + '/locale/') 39 | end 40 | 41 | $:.push('..') 42 | require_relative 'date_locale' 43 | 44 | require_relative 'html' 45 | require_relative 'poll' 46 | require_relative 'config_defaults' 47 | require_relative 'charset' 48 | 49 | class Dudle 50 | attr_reader :html, :table, :urlsuffix, :css, :user_css, :title, :tab 51 | 52 | def is_poll? 53 | $is_poll 54 | end 55 | 56 | def tabs_to_html(active_tab) 57 | ret = "' 69 | ret 70 | end 71 | 72 | def inittabs 73 | @tabs = [] 74 | @tabs << [_('Home'), @basedir] 75 | if is_poll? 76 | @tabs << ['', ''] 77 | @tabs << [_('Poll'), '.'] 78 | @tabs << [_('History'), 'history.cgi'] 79 | @tabs << ['', ''] 80 | @configtabs = [ 81 | [_('Edit Columns'), 'edit_columns.cgi'], 82 | [_('Invite Participants'), 'invite_participants.cgi'], 83 | [_('Access Control'), 'access_control.cgi'], 84 | [_('Overview'), 'overview.cgi'] 85 | ] 86 | @tabs += @configtabs 87 | @tabs << [_('Delete Poll'), 'delete_poll.cgi'] 88 | @tabs << ['', ''] 89 | else 90 | @tabs << [_('Examples'), 'example.cgi'] 91 | @tabs << [_('About'), 'about.cgi'] 92 | end 93 | @tabs << [_('Customize'), 'customize.cgi'] 94 | @tabtitle = @tabs.collect { |title, file| title if file == @tab }.compact[0] 95 | end 96 | 97 | def revision 98 | @requested_revision || VCS.revno 99 | end 100 | 101 | def breadcrumbs 102 | crumbs = $conf.breadcrumbs 103 | crumbs << ("" + _('DuD-Poll Home') + '') 104 | if is_poll? 105 | if @tab == '.' 106 | crumbs << CGI.escapeHTML(@title) 107 | else 108 | crumbs << "#{CGI.escapeHTML(@title)}" 109 | crumbs << @tabtitle 110 | end 111 | elsif @tab != '.' 112 | crumbs << @tabtitle 113 | end 114 | "" 115 | end 116 | 117 | def polltypespan 118 | return unless is_poll? 119 | 120 | "
#{CGI.escapeHTML(@polltype)}
" 121 | end 122 | 123 | def initialize(params = { revision: nil, title: nil, hide_lang_chooser: nil, relative_dir: '', load_extensions: true }) 124 | @requested_revision = params[:revision] 125 | @hide_lang_chooser = params[:hide_lang_chooser] 126 | @cgi = $cgi 127 | @tab = File.basename($0) 128 | @tab = '.' if @tab == 'index.cgi' 129 | 130 | if is_poll? 131 | # log last read access manually (no need to grep server logfiles) 132 | File.open('last_read_access', 'w').close unless @cgi.user_agent =~ $conf.bots 133 | @basedir = '..' 134 | inittabs 135 | if params[:revision] 136 | @table = YAML.load(VCS.cat(revision, 'data.yaml')) 137 | else 138 | @table = YAML.load_file('data.yaml') 139 | end 140 | @urlsuffix = File.basename(File.expand_path('.')) 141 | @title = @table.name 142 | 143 | if @table.head.to_s.include? 'TimePollHead' 144 | @polltype = _('This is a Event-scheduling poll.') 145 | else @table.head.to_s.include? 'PollHead' 146 | @polltype = _('This is a Normal poll.') 147 | end 148 | 149 | configfiles = @configtabs.collect { |_name, file| file } 150 | @is_config = configfiles.include?(@tab) 151 | @wizzardindex = configfiles.index(@tab) if @is_config 152 | 153 | @html = HTML.new("DuD-Poll - #{@title} - #{@tabtitle}", params[:relative_dir]) 154 | @html.add_html_head('') 155 | @html.header['Cache-Control'] = 'no-cache' 156 | else 157 | @basedir = '.' 158 | inittabs 159 | @title = params[:title] || "DuD-Poll - #{@tabtitle}" 160 | @html = HTML.new(@title, params[:relative_dir]) 161 | end 162 | 163 | @css = %w[default classic print].collect { |f| f + '.css' } 164 | if Dir.exist?("#{@basedir}/css/") 165 | Dir.open("#{@basedir}/css/").each { |f| 166 | if f =~ /\.css$/ 167 | @css << "css/#{f}" 168 | end 169 | } 170 | end 171 | if $cgi.include?('css') 172 | @user_css = $cgi['css'] 173 | @html.add_cookie('css', @user_css, '/', Time.now + (1 * 60 * 60 * 24 * 365 * (@user_css == $conf.default_css ? -1 : 1))) 174 | else 175 | @user_css = $cgi.cookies['css'][0] 176 | @user_css ||= $conf.default_css 177 | end 178 | 179 | if $cgi.user_agent =~ /.*MSIE [567]\..*/ 180 | css = [@user_css] 181 | else 182 | css = @css 183 | end 184 | @html.add_css("#{@basedir}/accessibility.css") 185 | css.each { |href| 186 | @html.add_css("#{@basedir}/#{href}", href.scan(%r{([^/]*)\.css}).flatten[0], href == @user_css) 187 | } 188 | 189 | @html << <
191 | HEAD 192 | $conf.header.each { |h| @html << h } 193 | 194 | @html << < 197 | #{tabs_to_html(@tab)} 198 |
199 |

#{CGI.escapeHTML(@title)} 200 |

201 | #{polltypespan} 202 | HEAD 203 | 204 | ################### 205 | # init extenisons # 206 | ################### 207 | @extensions = [] 208 | $d = self # FIXME: this is dirty, but extensions need to know table elem 209 | return unless Dir.exist?("#{@basedir}/extensions/") && params[:load_extensions] 210 | 211 | Dir.open("#{@basedir}/extensions/").sort.each { |f| 212 | next unless File.exist?("#{@basedir}/extensions/#{f}/main.rb") 213 | 214 | @extensions << f 215 | if File.exist?("#{@basedir}/extensions/#{f}/preload.rb") 216 | $current_ext_dir = f 217 | require "#{@basedir}/extensions/#{f}/preload" 218 | end 219 | } 220 | end 221 | 222 | def wizzard_nav 223 | ret = "
" 224 | [[_('Previous'), @wizzardindex == 0], 225 | [_('Next'), @wizzardindex >= @configtabs.size - 2], 226 | [_('Finish'), @wizzardindex == @configtabs.size - 1]].each { |button, disabled| 227 | ret += < 229 |
230 |
231 | 232 | 233 |
234 | 235 | 236 | READY 237 | } 238 | ret += '
' 239 | end 240 | 241 | def wizzard_redirect 242 | [[_('Previous'), @wizzardindex - 1], 243 | [_('Next'), @wizzardindex + 1], 244 | [_('Finish'), @configtabs.size - 1]].each { |action, linkindex| 245 | next unless $cgi.include?(action) 246 | 247 | @html.header['status'] = 'REDIRECT' 248 | @html.header['Cache-Control'] = 'no-cache' 249 | @html.header['Location'] = @configtabs[linkindex][1] 250 | @html << (_('All changes were saved successfully.') + " " + _('Proceed!') + '') 251 | out 252 | exit 253 | } 254 | end 255 | 256 | def out 257 | @html << wizzard_nav if @is_config 258 | 259 | @html.add_cookie('lang', @cgi['lang'], '/', Time.now + (1 * 60 * 60 * 24 * 365)) if @cgi.include?('lang') 260 | @html << '
' # content 261 | @html << "
" # languageChooser 300 | 301 | @html << '' # main 302 | $conf.footer.each { |f| @html << f } 303 | @extensions.each { |e| 304 | if File.exist?("#{@basedir}/extensions/#{e}/main.rb") 305 | $current_ext_dir = e 306 | require "#{@basedir}/extensions/#{e}/main" 307 | end 308 | } 309 | 310 | @html << '' 311 | @html.out(@cgi) 312 | end 313 | 314 | def <<(htmlbodytext) 315 | @html << htmlbodytext 316 | end 317 | end 318 | -------------------------------------------------------------------------------- /dudle_cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DAYS=90 4 | 5 | # installation path 6 | DUDLE_DIR=/var/www/dudle/ 7 | 8 | for i in `find $DUDLE_DIR -maxdepth 2 -name last_read_access -mtime +$DAYS`;do 9 | echo "[`date --rfc-3339=seconds`] `ls -l $i`" 10 | rm -rf "`dirname "$i"`" 11 | done 12 | 13 | # clean up manually deleted polls 14 | for i in `find /tmp/ -maxdepth 2 \! -readable -prune -o -name last_read_access -mtime +$DAYS -print`;do 15 | echo "[`date --rfc-3339=seconds`] `ls -l $i`" 16 | rm -rf "`dirname "$i"`" 17 | done 18 | -------------------------------------------------------------------------------- /edit_columns.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | load '../dudle.rb' 25 | 26 | revbeforeedit = VCS.revno 27 | 28 | if $cgi.include?('undo_revision') && $cgi['undo_revision'].to_i < revbeforeedit 29 | undorevision = $cgi['undo_revision'].to_i 30 | $d = Dudle.new(revision: undorevision) 31 | comment = $cgi.include?('redo') ? 'Redo changes' : 'Reverted Poll' 32 | $d.table.store("#{comment} to version #{undorevision}") 33 | else 34 | $d = Dudle.new 35 | end 36 | 37 | $d.table.edit_column($cgi['columnid'], $cgi['new_columnname'], $cgi) if $cgi.include?('new_columnname') 38 | $d.table.delete_column($cgi['deletecolumn']) if $cgi.include?('deletecolumn') 39 | 40 | $d.wizzard_redirect 41 | 42 | revno = VCS.revno 43 | 44 | $d << ('

' + _('Add and remove columns') + '

') 45 | $d << $d.table.edit_column_htmlform($cgi['editcolumn'], revno) 46 | 47 | h = VCS.history 48 | urevs = h.undorevisions 49 | rrevs = h.redorevisions 50 | 51 | disabled = {} 52 | title = {} 53 | undorevision = {} 54 | hidden = {} 55 | hidden['common'] = '' 56 | %w[add_remove_column_month firsttime lasttime].each { |v| 57 | hidden['common'] += "" if $cgi.include?(v) 58 | } 59 | %w[Undo Redo].each { |button| 60 | disabled[button] = "disabled='disabled'" 61 | } 62 | if urevs.max 63 | # enable undo 64 | disabled['Undo'] = '' 65 | undorevision['Undo'] = urevs.max.rev - 1 66 | 67 | coltitle, action = urevs.max.comment.scan(/^Column (.*) (added|deleted|edited)$/).flatten 68 | case action 69 | when 'added' 70 | title['Undo'] = _('Delete column') + " #{coltitle}" 71 | when 'deleted' 72 | title['Undo'] = _('Add column') + " #{coltitle}" 73 | when 'edited' 74 | title['Undo'] = _('Edit column') + " #{coltitle}" 75 | end 76 | 77 | urevs.max.rev + 1 if rrevs.min 78 | end 79 | if rrevs.min 80 | # enable redo 81 | disabled['Redo'] = '' 82 | undorevision['Redo'] = rrevs.min.rev 83 | 84 | coltitle, action = rrevs.min.comment.scan(/^Column (.*) (added|deleted|edited)$/).flatten 85 | case action 86 | when 'added' 87 | title['Redo'] = _('Add column') + " #{coltitle}" 88 | when 'deleted' 89 | title['Redo'] = _('Delete column') + " #{coltitle}" 90 | when 'edited' 91 | title['Redo'] = _('Edit column') + " #{coltitle}" 92 | end 93 | 94 | hidden['Redo'] = "" 95 | end 96 | 97 | $d << < 99 | 100 | 101 | UNDOREDOREADY 102 | localstr = { 'Undo' => _('Undo'), 'Redo' => _('Redo') } 103 | %w[Undo Redo].each { |button| 104 | $d << < 115 | TD 116 | } 117 | $d << < 119 |
106 |
107 |
108 | 109 | 110 | #{hidden['common']} 111 | #{hidden[button]} 112 |
113 |
114 |
120 | 121 | END 122 | 123 | # $d << (urevs + rrevs).to_html(curundorev,"") 124 | 125 | $d.out 126 | end 127 | -------------------------------------------------------------------------------- /error.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | require_relative 'dudle' 23 | 24 | if File.exist?("#{Dir.pwd}/#{File.dirname(ENV.fetch('REDIRECT_URL', nil))}/data.yaml") 25 | $d = Dudle.new(title: _('Error'), hide_lang_chooser: true, load_extensions: false, relative_dir: '../') 26 | else 27 | $d = Dudle.new(title: _('Error'), hide_lang_chooser: true, load_extensions: false) 28 | end 29 | 30 | if File.exist?($conf.errorlog) 31 | begin 32 | a = File.open($conf.errorlog, 'r').to_a 33 | rescue Exception => e 34 | errorstr = "Exception while opening #{$conf.errorlog}:\n#{e}" 35 | else 36 | s = [a.pop] 37 | s << a.pop while s.last.scan(/^\[([^\]]*)\] \[/).flatten[0] == a.last.scan(/^\[([^\]]*)\] \[/).flatten[0] || a.last =~ /^[^\[]/ 38 | errorstr = s.reverse.join 39 | end 40 | 41 | errormessage = "\n" + _('The following error was printed:') + "\n" + errorstr 42 | 43 | end 44 | 45 | errormessagebody = format(_("Hi!\n\nI found a bug in your application at %s.\nI did the following:\n\n\n\n\nI am using \n%s\nSincerely,\n"), errormessage: errormessage, urlofsite: $conf.siteurl) 46 | subject = _('Bug in DuD-Poll') 47 | 48 | $d << (format(_('An error occurred while executing DuD-Poll.
Please send an error report, including your browser, operating system, and what you did to %s.'), admin: "#{$conf.bugreportmail}")) 49 | 50 | if errorstr 51 | errorheadstr = _('Please include the following as well:') 52 | $d << < 54 | #{errorheadstr} 55 |
#{CGI.escapeHTML(errorstr)}
56 | ERROR 57 | end 58 | 59 | $d.out 60 | 61 | known = false 62 | if errorstr 63 | $conf.known_errors.each { |err| 64 | known = true if errorstr.index(err) 65 | } 66 | end 67 | 68 | if $conf.auto_send_report && !known 69 | tmpfile = "/tmp/error.#{rand(10_000)}" 70 | File.open(tmpfile, 'w') { |f| 71 | f << errorstr 72 | } 73 | 74 | `mail -s "Bug in DuD-Poll" #{$conf.bugreportmail} < #{tmpfile}` 75 | 76 | File.delete(tmpfile) 77 | 78 | end 79 | -------------------------------------------------------------------------------- /example.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2010-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | require_relative 'dudle' 25 | 26 | $d = Dudle.new 27 | 28 | if $cgi.include?('poll') 29 | 30 | poll = nil 31 | $conf.examples.each { |p| 32 | poll = p if $cgi['poll'] == p[:url] 33 | } 34 | 35 | if poll 36 | 37 | targeturl = poll[:url] 38 | 39 | if poll[:new_environment] 40 | targeturl += "_#{Time.now.to_i}" 41 | 42 | while File.exist?(targeturl) 43 | targeturl += 'I' 44 | end 45 | VCS.branch(poll[:url], targeturl) 46 | end 47 | 48 | if poll[:revno] 49 | Dir.chdir(targeturl) 50 | VCS.reset(poll[:revno]) 51 | Dir.chdir('..') 52 | end 53 | 54 | $d.html.header['status'] = 'REDIRECT' 55 | $d.html.header['Cache-Control'] = 'no-cache' 56 | $d.html.header['Location'] = $conf.siteurl + targeturl 57 | 58 | else 59 | $d << "
" 60 | $d << (format(_('Example not found: %s'), example: CGI.escapeHTML($cgi['poll']))) 61 | $d << '
' 62 | end 63 | end 64 | 65 | unless $d.html.header['status'] == 'REDIRECT' 66 | unless $conf.examples.empty? 67 | $d << ("

" + _('Examples') + '

') 68 | $d << '

' 69 | $d << _('If you want to play with the application, you may want to take a look at these example polls:') 70 | $d << '

' 71 | $d << '
    ' 72 | $conf.examples.each { |poll| 73 | $d << "
  • #{poll[:description]}
  • " unless poll[:hidden] 74 | } 75 | $d << '
' 76 | end 77 | 78 | $d << $conf.examplenotice 79 | end 80 | 81 | $d.out 82 | 83 | end 84 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellerben/dudle/76274146dde6888d47ad8da330217094c30a4894/favicon.ico -------------------------------------------------------------------------------- /hash.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | class Hash 21 | #################################################### 22 | # compare self with other hash # 23 | # in the order of fieldarray # 24 | # {1=>a,2=>b}.compare_by_values({1=>c,2=>d},[2,1]) # 25 | # -> compares b,d and afterwards a,c # 26 | #################################################### 27 | def compare_by_values(other, fieldarray) 28 | return 0 if fieldarray.size == 0 29 | return 1 if self[fieldarray[0]].nil? 30 | return -1 if other[fieldarray[0]].nil? 31 | 32 | if self[fieldarray[0]] == other[fieldarray[0]] 33 | return 0 if fieldarray.size == 1 34 | 35 | compare_by_values(other, fieldarray[1, fieldarray.size - 1]) 36 | 37 | else 38 | self[fieldarray[0]] <=> other[fieldarray[0]] 39 | end 40 | end 41 | end 42 | 43 | if __FILE__ == $0 44 | require 'test/unit' 45 | class Hash_test < Test::Unit::TestCase 46 | def test_compare_by_values 47 | a = { 1 => 1, 2 => 2, 3 => 3 } 48 | b = { 1 => 1, 2 => 2, 3 => 2 } 49 | c = { 1 => 1, 2 => 3, 3 => 3 } 50 | d = { 1 => 1, 2 => 3 } 51 | 52 | assert_equal(0, a.compare_by_values(a, [1, 2, 3])) 53 | assert_equal(1, a.compare_by_values(b, [1, 2, 3])) 54 | assert_equal(-1, a.compare_by_values(c, [1, 2, 3])) 55 | assert_equal(-1, c.compare_by_values(d, [1, 2, 3])) 56 | assert_equal(1, d.compare_by_values(c, [1, 2, 3])) 57 | assert_equal(0, d.compare_by_values(c, [])) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /history.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | load '../dudle.rb' 24 | 25 | if $cgi.include?('revision') 26 | revno = $cgi['revision'].to_i 27 | $d = Dudle.new(revision: revno) 28 | versiontitle = format(_('Poll of version %s'), revisionnumber: revno) 29 | else 30 | revno = VCS.revno 31 | $d = Dudle.new 32 | versiontitle = format(_('Current poll (version %s)'), revisionnumber: revno) 33 | end 34 | 35 | historystr = _('History') 36 | $d << <#{versiontitle} 38 | #{$d.table.to_html(false)} 39 | 40 | #{$d.table.comment_to_html(false)} 41 |

#{historystr}

42 |
43 | #{$d.table.history_selectform($cgi.include?('revision') ? revno : nil, $cgi['history'])} 44 | 45 | #{$d.table.history_to_html(revno, $cgi['history'])} 46 |
47 | HTML 48 | 49 | $d.out 50 | end 51 | -------------------------------------------------------------------------------- /html.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | class HTML 21 | attr_accessor :body, :header 22 | attr_reader :relative_dir 23 | 24 | def initialize(title, relative_dir = '') 25 | @title = title 26 | @relative_dir = relative_dir 27 | @header = {} 28 | @header['type'] = 'text/html' 29 | # @header["type"] = "application/xhtml+xml" 30 | @header['charset'] = 'utf-8' 31 | 32 | @body = '' 33 | @htmlheader = '' 34 | @css = [] 35 | @atom = [] 36 | end 37 | 38 | def head 39 | ret = < 41 | 42 | 43 | #{@title} 44 | 45 | HEAD 46 | 47 | @css = [@css[0]] + @css[1..-1].sort unless @css.empty? 48 | @css.each { |title, href| 49 | titleattr = "title='#{title}'" if title != '' 50 | ret += "\n" 51 | ret += "\n" if title == 'print' 52 | } 53 | 54 | @atom.each { |href| 55 | ret += "\n" 56 | } 57 | 58 | ret += @htmlheader 59 | ret += '' 60 | ret 61 | end 62 | 63 | def add_css(href, title = '', default = false) 64 | if default 65 | @css.unshift([title, href]) 66 | else 67 | @css << [title, href] 68 | end 69 | end 70 | 71 | def add_atom(href) 72 | @atom << href 73 | end 74 | 75 | def add_cookie(key, value, path, expiretime) 76 | c = CGI::Cookie.new(key, value) 77 | c.path = path 78 | c.expires = expiretime 79 | @header['cookie'] ||= [] 80 | @header['cookie'] << c 81 | end 82 | 83 | def add_head_script(file) 84 | add_html_head("") 85 | end 86 | 87 | def add_script_file(file) 88 | self << "" 89 | end 90 | 91 | def add_script(script) 92 | self << < 98 | SCRIPT 99 | end 100 | 101 | def <<(bodycontent) 102 | @body += bodycontent.chomp + "\n" 103 | end 104 | 105 | def add_html_head(headercontent) 106 | @htmlheader += headercontent.chomp + "\n" 107 | end 108 | 109 | def out(cgi) 110 | # FIXME: quick and dirty fix for encoding problem 111 | { 112 | 'ö' => 'ö', 113 | 'ü' => 'ü', 114 | 'ä' => 'ä', 115 | 'Ö' => 'Ö', 116 | 'Ü' => 'Ü', 117 | 'Ä' => 'Ä', 118 | 'ß' => 'ß', 119 | '–' => '–', 120 | '„' => '„', 121 | '“' => '“', 122 | '”' => '”', 123 | '✔' => '✔', 124 | '✘' => '✘', 125 | '◀' => '◀', 126 | '▶' => '▶', 127 | '✍' => '✍', 128 | '✖' => '✖', 129 | '•' => '•', 130 | '▾' => '▾', 131 | '▴' => '▴' 132 | }.each { |from, to| 133 | @body.gsub!(from, to) 134 | } 135 | # @body.gsub!(/./){|char| 136 | # code = char[0] 137 | # code > 127 ? "&##{code};" : char 138 | # } 139 | xmllang = _("xml:lang='en' lang='en' dir='ltr'") 140 | cgi.out(@header) { 141 | < 144 | 145 | #{head} 146 | #{@body} 147 | 148 | HEAD 149 | } 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /index.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | require_relative 'dudle' 25 | 26 | $d = Dudle.new 27 | 28 | if $cgi.include?('create_poll') && $cgi.include?('poll_url') 29 | polltitle = $cgi['create_poll'] 30 | if polltitle == '' 31 | createnotice = _('Please enter a descriptive title.') 32 | else 33 | if $cgi['poll_url'] == '' 34 | require 'securerandom' 35 | true while File.exist?(pollurl = SecureRandom.urlsafe_base64($conf.random_chars)) 36 | else 37 | pollurl = $cgi['poll_url'] 38 | end 39 | 40 | if !(pollurl =~ /\A[\w\-_]*\Z/) 41 | createnotice = _('Custom address may only contain letters, numbers, and dashes.') 42 | elsif File.exist?(pollurl) 43 | createnotice = _('A poll with this address already exists.') 44 | else 45 | Dir.mkdir(pollurl) 46 | Dir.chdir(pollurl) 47 | begin 48 | VCS.init 49 | File.symlink('../participate.rb', 'index.cgi') 50 | %w[atom customize history overview edit_columns access_control delete_poll invite_participants].each { |f| 51 | File.symlink("../#{f}.rb", "#{f}.cgi") 52 | } 53 | ['data.yaml', '.htaccess', '.htdigest'].each { |f| 54 | File.open(f, 'w').close 55 | VCS.add(f) 56 | } 57 | Poll.new(polltitle, $cgi['poll_type']) 58 | Dir.chdir('..') 59 | $d.html.header['status'] = 'REDIRECT' 60 | $d.html.header['Cache-Control'] = 'no-cache' 61 | $d.html.header['Location'] = $conf.siteurl + pollurl + '/edit_columns.cgi' 62 | $d << (format(_('The poll was created successfully. The link to your new poll is: %s'), link: "
#{pollurl}")) 63 | rescue WrongPollTypeError # should only happen in case of hacking 64 | Dir.chdir('..') 65 | require 'fileutils' 66 | FileUtils.rm_r(pollurl) 67 | $d.html.header['status'] = 'REDIRECT' 68 | $d.html.header['Cache-Control'] = 'no-cache' 69 | $d.html.header['Location'] = 'http://localhost/' 70 | $d << _('Go away.') 71 | end 72 | end 73 | end 74 | end 75 | 76 | unless $d.html.header['status'] == 'REDIRECT' 77 | 78 | $d << ('

' + _('Create new poll') + '

') 79 | 80 | titlestr = _('Title') 81 | typestr = _('Type') 82 | timepollstr = _('Event-scheduling poll (e. g., schedule a meeting)') 83 | normalpollstr = _('Normal poll (e. g., vote for what is the best coffee)') 84 | customaddrstr = _('Custom address (optional)') 85 | customaddrhintstr = _('May contain letters, numbers, and dashes.') 86 | 87 | createstr = _('Create') 88 | $d << < 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 104 | 105 | 106 | 108 | 109 | 110 | 112 | 113 |
#{typestr}: 98 | 99 | 100 |
101 | 102 | 103 |
#{customaddrstr}: 107 | #{customaddrhintstr}
111 |
114 |
115 | #{createnotice} 116 |
117 | 118 | 119 | CREATE 120 | 121 | $d << $conf.indexnotice 122 | end 123 | 124 | $d.out 125 | end 126 | -------------------------------------------------------------------------------- /invite_participants.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | load '../dudle.rb' 25 | 26 | $d = Dudle.new 27 | 28 | unless $cgi.include?('cancel') 29 | if $cgi.include?('deleteuser') 30 | $d.table.delete($cgi['edituser']) 31 | elsif $cgi.include?('add_participant') 32 | $d.table.add_participant($cgi['olduser'], $cgi['add_participant'], {}) 33 | end 34 | end 35 | 36 | $d.wizzard_redirect 37 | 38 | inviteparticipantsstr = _('Invite Participants') 39 | $d << <#{inviteparticipantsstr} 41 | 42 | #{$d.table.invite_to_html} 43 | 44 | TABLE 45 | 46 | $d.out 47 | end 48 | -------------------------------------------------------------------------------- /log.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | require 'time' 21 | 22 | class LogEntry 23 | attr_accessor :rev, :timestamp, :comment 24 | 25 | def initialize(rev, timestamp, comment) 26 | @rev = rev 27 | @timestamp = timestamp 28 | @comment = comment 29 | end 30 | 31 | def to_html(link = true, history = '') 32 | ret = '' 110 | ret += "' 116 | ret 117 | end 118 | 119 | def to_html(showparticipation = true) 120 | # border=1 for textbrowsers ;--) 121 | ret = "
' 33 | if link 34 | ret += "(other) 48 | rev <=> other.rev 49 | end 50 | 51 | def inspect 52 | "#{@rev}: #{@comment}" 53 | end 54 | end 55 | 56 | class Log 57 | attr_reader :log 58 | 59 | def initialize(a = []) 60 | @log = a.compact.sort 61 | end 62 | 63 | def min 64 | @log.sort! 65 | @log[0] 66 | end 67 | 68 | def max 69 | @log.sort! 70 | @log[-1] 71 | end 72 | 73 | def revisions 74 | @log.collect { |e| e.rev } 75 | end 76 | 77 | def [](revision) 78 | i = revisions.index(revision) 79 | return @log[i] if i 80 | 81 | # no revision found, search the nearest 82 | dist = revisions.collect { |e| (e - revision).abs }.sort[0] 83 | return nil unless dist 84 | 85 | i = revisions.index(revision + dist) || revisions.index(revision - dist) 86 | @log[i] 87 | end 88 | 89 | def size 90 | @log.size 91 | end 92 | 93 | def around_rev(rev, number) 94 | ret = [self[rev]] 95 | midindex = @log.index(ret[0]) 96 | counter = 1 97 | while ret.size < number && counter < @log.size 98 | ret << @log[midindex + counter] 99 | counter *= -1 if counter <= midindex 100 | if counter > 0 101 | counter += 1 102 | end 103 | ret.compact! 104 | end 105 | ret.sort! 106 | Log.new(ret) 107 | end 108 | 109 | def +(other) 110 | a = @log + other.log 111 | Log.new(a.sort) 112 | end 113 | 114 | def add(revision, timestamp, comment) 115 | @log << LogEntry.new(revision, timestamp, comment) 116 | @log.sort! 117 | end 118 | 119 | def to_html(unlinkedrevision, history) 120 | ret = '' 122 | reverse_each { |l| 123 | ret += l.to_html(unlinkedrevision != l.rev, history) 124 | } 125 | ret += '
' 121 | ret += _('Version') + '' + _('Date') + '' + _('Comment') + '
' 126 | ret 127 | end 128 | 129 | def reverse_each(&block) 130 | @log.reverse.each(&block) 131 | end 132 | 133 | def each(&block) 134 | @log.each(&block) 135 | end 136 | 137 | def collect(&block) 138 | @log.collect(&block) 139 | end 140 | 141 | def comment_matches(regex) 142 | Log.new(@log.collect { |e| e if e.comment =~ regex }.compact) 143 | end 144 | 145 | def undorevisions 146 | h = [] 147 | minrev = min.rev 148 | rev = max.rev 149 | while rev > minrev 150 | elem = self[rev] 151 | prevrev = elem.comment.scan(/^.* to version (\d*)$/).flatten[0] 152 | if prevrev 153 | rev = prevrev.to_i 154 | else 155 | h << elem 156 | rev -= 1 157 | end 158 | end 159 | h.sort! 160 | a = [] 161 | begin 162 | a << h.pop 163 | end while a.last && a.last.comment =~ /^Column .*$/ 164 | a.pop 165 | a.sort! 166 | Log.new(a) 167 | end 168 | 169 | def redorevisions 170 | @log.sort! 171 | revertrevs = [] 172 | redone = [] 173 | minrev = min.rev 174 | (minrev..max.rev).to_a.reverse.each { |rev| 175 | action, r = self[rev].comment.scan(/^(.*) to version (\d*)$/).flatten 176 | break unless r 177 | 178 | if action =~ /^Redo changes/ 179 | break unless revertrevs.empty? 180 | 181 | redone << (r.to_i - 1) 182 | else 183 | revertrevs << r.to_i 184 | end 185 | } 186 | revertrevs -= redone 187 | Log.new(revertrevs.collect { |e| self[e + 1] }) 188 | end 189 | end 190 | 191 | if __FILE__ == $0 192 | require 'test/unit' 193 | require 'pp' 194 | class Log_test < Test::Unit::TestCase 195 | def test_indexes 196 | l = Log.new 197 | 198 | l.each { flunk('this should not happen') } 199 | assert_equal(nil, l[2]) 200 | 201 | l.add(10, Time.now, 'baz 10') 202 | 20.times { |i| 203 | l.add(i, Time.now, "foo #{i}") unless i == 10 204 | } 205 | 206 | assert_equal(0, l.min.rev) 207 | assert_equal(19, l.max.rev) 208 | assert_equal('baz 10', l[10].comment) 209 | 210 | assert_equal([10], l.comment_matches(/^baz \d*$/).revisions) 211 | 212 | [42, 23].each { |i| 213 | l.add(i, Time.now, "foo #{i}") 214 | } 215 | assert_equal(l[42], l[37]) 216 | 217 | assert_equal([16, 17, 18, 19, 23, 42], l.around_rev(23, 6).revisions) 218 | assert_equal([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], l.around_rev(2, 12).revisions) 219 | assert_equal(l.revisions, l.around_rev(0, 99).revisions) 220 | end 221 | 222 | def test_undoredo 223 | # 15 16 17 224 | # | | | 225 | # 11 10 | 226 | # 14| | | 227 | # 13| 7---8---9 228 | # | 12| 6 229 | # | | | | 230 | # 0-1-2-3-4-5 231 | # p 232 | 233 | def dummy_add(log, comment) 234 | log.add(log.max.rev + 1, Time.now, comment) 235 | end 236 | l = Log.new 237 | l.add(1, Time.now, 'Participant Spamham added') 238 | 6.times { |i| 239 | l.add(i, Time.now, "Column Foo#{i} added") unless i == 1 240 | } 241 | 242 | assert_equal([2, 3, 4, 5], l.undorevisions.revisions) 243 | assert_equal([], l.redorevisions.revisions) 244 | 245 | dummy_add(l, 'Reverted Poll to version 4') 246 | assert_equal([2, 3, 4], l.undorevisions.revisions) 247 | assert_equal([5], l.redorevisions.revisions) 248 | 249 | dummy_add(l, 'Reverted Poll to version 3') 250 | assert_equal([2, 3], l.undorevisions.revisions) 251 | assert_equal([4, 5], l.redorevisions.revisions) 252 | 253 | dummy_add(l, 'Column Foo added') 254 | assert_equal([2, 3, 8], l.undorevisions.revisions) 255 | assert_equal([], l.redorevisions.revisions) 256 | 257 | dummy_add(l, 'Column Foo added') 258 | 259 | assert_equal([2, 3, 8, 9], l.undorevisions.revisions) 260 | assert_equal([], l.redorevisions.revisions) 261 | 262 | dummy_add(l, 'Reverted Poll to version 8') 263 | dummy_add(l, 'Reverted Poll to version 7') 264 | dummy_add(l, 'Reverted Poll to version 2') 265 | dummy_add(l, 'Reverted Poll to version 1') 266 | assert_equal([], l.undorevisions.revisions) 267 | assert_equal([2, 3, 8, 9], l.redorevisions.revisions) 268 | 269 | dummy_add(l, 'Redo changes to version 2') 270 | dummy_add(l, 'Redo changes to version 3') 271 | assert_equal([2, 3], l.undorevisions.revisions) 272 | assert_equal([8, 9], l.redorevisions.revisions) 273 | 274 | dummy_add(l, 'Redo changes to version 8') 275 | dummy_add(l, 'Redo changes to version 9') 276 | assert_equal([2, 3, 8, 9], l.undorevisions.revisions) 277 | assert_equal([], l.redorevisions.revisions) 278 | 279 | # second time should be the same 280 | dummy_add(l, 'Reverted Poll to version 8') 281 | assert_equal([2, 3, 8], l.undorevisions.revisions) 282 | assert_equal([9], l.redorevisions.revisions) 283 | dummy_add(l, 'Reverted Poll to version 7') 284 | assert_equal([2, 3], l.undorevisions.revisions) 285 | assert_equal([8, 9], l.redorevisions.revisions) 286 | dummy_add(l, 'Reverted Poll to version 2') 287 | assert_equal([2], l.undorevisions.revisions) 288 | assert_equal([3, 8, 9], l.redorevisions.revisions) 289 | dummy_add(l, 'Reverted Poll to version 1') 290 | assert_equal([], l.undorevisions.revisions) 291 | assert_equal([2, 3, 8, 9], l.redorevisions.revisions) 292 | 293 | dummy_add(l, 'Redo changes to version 2') 294 | dummy_add(l, 'Redo changes to version 3') 295 | assert_equal([2, 3], l.undorevisions.revisions) 296 | assert_equal([8, 9], l.redorevisions.revisions) 297 | 298 | dummy_add(l, 'Redo changes to version 8') 299 | dummy_add(l, 'Redo changes to version 9') 300 | assert_equal([2, 3, 8, 9], l.undorevisions.revisions) 301 | assert_equal([], l.redorevisions.revisions) 302 | end 303 | end 304 | end 305 | -------------------------------------------------------------------------------- /maintenance.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2016-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | require_relative 'dudle' 23 | # check for trailing slash 24 | if ENV.fetch('REDIRECT_URL', nil) =~ /#{$cgi['poll']}$/ 25 | $d = Dudle.new(title: _('Maintenance'), hide_lang_chooser: true, relative_dir: "#{$cgi['poll']}/") 26 | else 27 | $d = Dudle.new(title: _('Maintenance'), hide_lang_chooser: true) 28 | end 29 | 30 | if File.exist?('maintenance.html') 31 | $d << _('This site is currently undergoing maintenance!') 32 | $d << File.read('maintenance.html') 33 | else 34 | $d << _('You should not browse to this file directly. Please create a file named "maintenance.html" to enable the maintenance mode.') 35 | end 36 | 37 | $d.out 38 | -------------------------------------------------------------------------------- /not_found.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if ENV['REDIRECT_URL'] 23 | require_relative 'dudle' 24 | if File.exist?(Dir.pwd + File.dirname(ENV.fetch('REDIRECT_URL', nil))) 25 | $d = Dudle.new(hide_lang_chooser: true, load_extensions: false) 26 | else 27 | $d = Dudle.new(hide_lang_chooser: true, load_extensions: false, relative_dir: '../') 28 | end 29 | 30 | _('Poll not found') 31 | 32 | str = [ 33 | _('The requested poll was not found.'), 34 | _('There are several reasons why a poll may have been deleted:'), 35 | _('Somebody clicked on “Delete poll” and deleted the poll manually.'), 36 | _('The poll was deleted by the administrator because it was not accessed for a long time.'), 37 | _('Return to DuD-Poll home and schedule a new poll') 38 | ] 39 | 40 | $d << < 42 | #{str[0]} 43 |

44 |

45 | #{str[1]} 46 |

    47 |
  • #{str[2]}
  • 48 |
  • #{str[3]}
  • 49 |
50 |
#{str[4]} 51 |

52 | END 53 | 54 | $d.out # ($cgi) 55 | 56 | else 57 | puts 58 | puts 'Not Found' 59 | end 60 | -------------------------------------------------------------------------------- /overview.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | 24 | load '../dudle.rb' 25 | 26 | $d = Dudle.new 27 | 28 | $d.wizzard_redirect 29 | 30 | $d << _('The next steps are:') 31 | 32 | sendlink = _('Send the link to all participants:') 33 | mailstr = _('Send this link via email...') 34 | nextstr = _('Visit the poll yourself:') 35 | subjectstr = format(_('Link to DuD-Poll about %s'), polltitle: $d.title) 36 | $d << < 38 |
  • 39 | #{sendlink} 40 | 44 |
  • 45 |
  • 46 | #{nextstr} 47 | 50 |
  • 51 | 52 | END 53 | 54 | $d.out 55 | end 56 | -------------------------------------------------------------------------------- /participate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############################################################################ 4 | # Copyright 2009-2019 Benjamin Kellermann # 5 | # # 6 | # This file is part of Dudle. # 7 | # # 8 | # Dudle is free software: you can redistribute it and/or modify it under # 9 | # the terms of the GNU Affero General Public License as published by # 10 | # the Free Software Foundation, either version 3 of the License, or # 11 | # (at your option) any later version. # 12 | # # 13 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 14 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 16 | # License for more details. # 17 | # # 18 | # You should have received a copy of the GNU Affero General Public License # 19 | # along with dudle. If not, see . # 20 | ############################################################################ 21 | 22 | if __FILE__ == $0 23 | load '../dudle.rb' 24 | $d = Dudle.new 25 | 26 | edit = false 27 | unless $cgi.include?('cancel') 28 | if $cgi.include?('delete_participant_confirm') 29 | $d.table.delete($cgi['delete_participant_confirm']) 30 | edit = true 31 | elsif $cgi.include?('add_participant') 32 | agreed = {} 33 | $cgi.params.each { |k, v| 34 | if k =~ /^add_participant_checked_/ 35 | agreed[k.gsub(/^add_participant_checked_/, '')] = v[0] 36 | end 37 | } 38 | 39 | $d.table.add_participant($cgi['olduser'], $cgi['add_participant'], agreed) 40 | edit = true 41 | end 42 | end 43 | 44 | if $cgi['comment'] != '' 45 | $d.table.add_comment($cgi['commentname'], $cgi['comment']) 46 | edit = true 47 | end 48 | 49 | if $cgi.include?('delete_comment') 50 | $d.table.delete_comment($cgi['delete_comment']) 51 | edit = true 52 | end 53 | 54 | if edit 55 | $d.html.header['status'] = 'REDIRECT' 56 | $d.html.header['Cache-Control'] = 'no-cache' 57 | $d.html.header['Location'] = $conf.siteurl 58 | $d << (format(_('The changes were saved, you should be redirected to %s.'), link: "#{$conf.siteurl}")) 59 | 60 | else 61 | 62 | $d.html.add_atom('atom.cgi') if File.exist?('../atom.rb') 63 | 64 | # TABLE 65 | $d << < 67 |
    68 | #{$d.table.to_html} 69 |
    70 | 71 | 72 | #{$d.table.comment_to_html} 73 | HTML 74 | 75 | end 76 | 77 | $d.out 78 | end 79 | -------------------------------------------------------------------------------- /poll.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | require_relative 'hash' 21 | require 'yaml' 22 | require 'time' 23 | require 'fileutils' 24 | require_relative 'pollhead' 25 | require_relative 'timepollhead' 26 | 27 | $KCODE = 'u' if RUBY_VERSION < '1.9.0' 28 | class String 29 | @@htmlidcache = {} 30 | @@htmlidncache = {} 31 | 32 | # FIXME: htmlid should not depend on the order it is requested 33 | def to_htmlID 34 | if @@htmlidcache[self] 35 | id = @@htmlidcache[self] 36 | else 37 | id = gsub(/[^A-Za-z0-9_\-]/, '_') 38 | if @@htmlidncache[id] 39 | @@htmlidncache[id] += 1 40 | id += @@htmlidncache[id].to_s 41 | end 42 | @@htmlidncache[id] = -1 43 | @@htmlidcache[self] = id 44 | end 45 | id 46 | end 47 | end 48 | 49 | class WrongPollTypeError < StandardError 50 | end 51 | 52 | class Poll 53 | attr_reader :head, :name 54 | 55 | YESVAL = 'a_yes__' 56 | MAYBEVAL = 'b_maybe' 57 | NOVAL = 'c_no___' 58 | 59 | @@table_html_hooks = [] 60 | def self.table_html_hooks 61 | @@table_html_hooks 62 | end 63 | 64 | def initialize(name, type) 65 | @name = name 66 | 67 | case type 68 | when 'normal' 69 | @head = PollHead.new 70 | when 'time' 71 | @head = TimePollHead.new 72 | else 73 | raise(WrongPollTypeError, "unknown poll type: #{type}") 74 | end 75 | @data = {} 76 | @comment = [] 77 | store "Poll #{name} created" 78 | end 79 | 80 | def sort_data(fields) 81 | if fields.include?('name') 82 | until fields.pop == 'name' 83 | end 84 | @data.sort { |x, y| 85 | cmp = x[1].compare_by_values(y[1], fields) 86 | cmp == 0 ? x[0].downcase <=> y[0].downcase : cmp 87 | } 88 | else 89 | @data.sort { |x, y| x[1].compare_by_values(y[1], fields) } 90 | end 91 | end 92 | 93 | def userstring(participant, link) 94 | ret = '' 95 | if link 96 | ret += "
    " 97 | ret += '" 102 | ret += EDIT 103 | ret += ' | " 108 | ret += "#{DELETE}" 109 | ret += '" 111 | else 112 | ret += "" 113 | end 114 | ret += "#{CGI.escapeHTML(participant)}" 115 | ret += '
    \n" 122 | 123 | sortcolumns = $cgi.include?('sort') ? $cgi.params['sort'] : ['timestamp'] 124 | ret += "#{@head.to_html(sortcolumns)}" 125 | ret += "" 126 | sort_data(sortcolumns).each { |participant, poll| 127 | if $cgi['edituser'] == participant 128 | ret += participate_to_html 129 | else 130 | ret += "\n" 131 | ret += userstring(participant, showparticipation) 132 | @head.columns.each { |column| 133 | case poll[column] 134 | when nil 135 | value = UNKNOWN 136 | klasse = 'undecided' 137 | str = _('Unknown') 138 | when /yes/ # allow anything containing yes (backward compatibility) 139 | value = YES 140 | klasse = YESVAL 141 | str = format(_('%s selected Yes'), user: CGI.escapeHTML(participant)) 142 | when /no/ 143 | value = NO 144 | klasse = NOVAL 145 | str = format(_('%s selected No'), user: CGI.escapeHTML(participant)) 146 | when /maybe/ 147 | value = MAYBE 148 | klasse = MAYBEVAL 149 | str = format(_('%s selected Maybe'), user: CGI.escapeHTML(participant)) 150 | end 151 | if column.to_s.match(/\d\d\d\d-\d\d-\d\d/) 152 | ret += "\n" 153 | else 154 | ret += "\n" 155 | end 156 | } 157 | ret += "" 158 | ret += "\n" 159 | end 160 | } 161 | 162 | @@table_html_hooks.each { |hook| ret += hook.call(ret) } 163 | 164 | ret += '' 165 | # PARTICIPATE 166 | ret += participate_to_html unless @data.keys.include?($cgi['edituser']) || !showparticipation 167 | 168 | # SUMMARY 169 | ret += "\n" 170 | @head.columns.each { |column| 171 | yes = 0 172 | undecided = 0 173 | @data.each_value { |participant| 174 | if participant[column] =~ /yes/ 175 | yes += 1 176 | elsif !participant.has_key?(column) or participant[column] =~ /maybe/ 177 | undecided += 1 178 | end 179 | } 180 | 181 | if @data.empty? 182 | percent_f = 0 183 | else 184 | percent_f = 100.0 * yes / @data.size 185 | end 186 | percent = "#{percent_f.round}%" unless @data.empty? 187 | if undecided > 0 188 | percent + "-#{(100.0 * (undecided + yes) / @data.size).round} %" 189 | end 190 | 191 | ofstr = _('out of') 192 | votedstr = _('participants voted yes') 193 | 194 | ret += "\n" 195 | } 196 | 197 | ret += "" 198 | ret += "
    #{str}#{str}#{poll['timestamp'].strftime('%c')}
    " + _('Total') + "#{yes}#{yes} #{ofstr} #{@data.size} #{votedstr}.
    \n" 199 | ret 200 | end 201 | 202 | def invite_to_html 203 | edituser = $cgi['edituser'] unless $cgi.include?('deleteuser') 204 | invitestr = _('Invite') 205 | namestr = _('Name') 206 | ret = < 208 | 209 | #{namestr} 210 | 211 | HEAD 212 | @data.keys.sort.each { |participant| 213 | has_voted = false 214 | @head.columns.each { |column| 215 | has_voted = true unless @data[participant][column].nil? 216 | } 217 | 218 | if edituser == participant 219 | ret += "" 220 | ret += add_participant_input(edituser) 221 | ret += save_input(edituser, invitestr) 222 | else 223 | ret += "" 224 | ret += userstring(participant, !has_voted) 225 | end 226 | ret += '' 227 | } 228 | unless @data.keys.include?(edituser) 229 | ret += "" 230 | ret += add_participant_input(edituser) 231 | ret += save_input(edituser, invitestr) 232 | ret += '' 233 | end 234 | ret += '' 235 | ret 236 | end 237 | 238 | def add_participant_input(edituser) 239 | < 241 | 242 | 249 | 250 | END 251 | end 252 | 253 | def save_input(edituser, savestring, changestr = _('Save changes')) 254 | ret = '' 255 | if @data.include?(edituser) 256 | ret += "" 257 | ret += "
    " 258 | else 259 | ret += "" 260 | end 261 | ret + "\n" 262 | end 263 | 264 | def participate_to_html 265 | ret = "\n" 266 | 267 | if $cgi.include?('deleteuser') && @data.include?($cgi['edituser']) 268 | ret += deleteuser_to_html 269 | else 270 | ret += edituser_to_html 271 | end 272 | ret + "\n" 273 | end 274 | 275 | def deleteuser_to_html 276 | ret = "\n" 277 | ret += "#{CGI.escapeHTML($cgi['edituser'])}" 278 | ret += "" 279 | ret += format(_('Do you really want to delete user %s?'), user: CGI.escapeHTML($cgi['edituser'])) 280 | ret += "" 281 | ret += '' 282 | ret += save_input($cgi['edituser'], '', _('Confirm')) 283 | ret += '' 284 | ret 285 | end 286 | 287 | def edituser_to_html 288 | edituser = $cgi['edituser'] 289 | checked = {} 290 | if @data.include?(edituser) 291 | @head.columns.each { |k| checked[k] = @data[edituser][k] } 292 | else 293 | edituser = $cgi.cookies['username'][0] unless @data.include?($cgi.cookies['username'][0]) 294 | @head.columns.each { |k| checked[k] = NOVAL } 295 | end 296 | 297 | ret = "\n" 298 | 299 | ret += add_participant_input(edituser) 300 | 301 | @head.columns.each { |column| 302 | ret += "" 303 | [[YES, YESVAL, _('Yes')], [NO, NOVAL, _('No')], [MAYBE, MAYBEVAL, _('Maybe')]].each { |valhuman, valbinary, valtext| 304 | if column.to_s.match(/\d\d\d\d-\d\d-\d\d/) 305 | ret += <<-TR 306 | 307 | 318 | 319 | TR 320 | else 321 | ret += <<-TR 322 | 323 | 334 | 335 | TR 336 | end 337 | } 338 | ret += '
    308 | 316 | 317 |
    324 | 332 | 333 |
    ' 339 | } 340 | ret += save_input(edituser, _('Save')) 341 | 342 | ret += "\n" 343 | 344 | ret 345 | end 346 | 347 | def comment_to_html(editable = true) 348 | ret = "
    " 349 | ret += '

    ' + _('Comments') if !@comment.empty? || editable 350 | if $cgi.include?('comments_reverse') 351 | ret += "" + ' ' + _('Sort oldest comment first') 352 | ret += "" 353 | else 354 | ret += "" + ' ' + _('Sort newest comment first') 355 | ret += "" 356 | end 357 | 358 | if @comment.size > 5 359 | ret += "" + ' ' + _('Go to last comment') 360 | ret += "" 361 | end 362 | 363 | ret += '

    ' if !@comment.empty? || editable 364 | 365 | unless @comment.empty? 366 | i = 0 # for commentanchor 367 | c = @comment.dup 368 | c.reverse! if $cgi.include?('comments_reverse') 369 | c.each { |time, name, comment| 370 | ret += "
    " 371 | ret += "

    " 372 | i += 1 373 | ret += format(_('%s said on %

    \n" 422 | ret 423 | end 424 | 425 | def history_selectform(revision, selected) 426 | showhiststr = _('Show history items:') 427 | ret = <
    429 |
    430 | 431 | ' 441 | ret += "" if revision 442 | updatestr = _('Update') 443 | ret += < 445 |
    446 |
    447 | FORM 448 | ret 449 | end 450 | 451 | def history_to_html(middlerevision, only) 452 | log = VCS.history 453 | if only != '' 454 | case only 455 | when 'comments' 456 | match = /^Comment .*$/ 457 | when 'participants' 458 | match = /^Participant .*$/ 459 | when 'columns' 460 | match = /^Column .*$/ 461 | when 'ac' 462 | match = /^Access Control .*$/ 463 | end 464 | log = log.comment_matches(match) 465 | end 466 | log.around_rev(middlerevision, 11).to_html(middlerevision, only) 467 | end 468 | 469 | def add_participant(olduser, name, agreed) 470 | name.strip! 471 | if name == '' 472 | maximum = @data.keys.collect { |e| e.scan(/^Anonymous #(\d*)/).flatten[0] }.compact.collect { |i| i.to_i }.max 473 | maximum ||= 0 474 | name = "Anonymous ##{maximum + 1}" 475 | end 476 | action = '' 477 | if @data.delete(olduser) 478 | action = 'edited' 479 | else 480 | action = 'added' 481 | end 482 | @data[name] = { 'timestamp' => Time.now } 483 | @head.columns.each { |column| 484 | @data[name][column] = agreed[column.to_s] 485 | } 486 | store "Participant #{name.strip} #{action}" 487 | end 488 | 489 | def delete(name) 490 | return unless @data.has_key?(name) 491 | 492 | @data.delete(name) 493 | store "Participant #{name.strip} deleted" 494 | end 495 | 496 | def store(comment) 497 | lockfile = 'data.yaml.lock' 498 | 499 | File.open(lockfile, 'w') { |lock| 500 | if lock.flock(File::LOCK_EX) 501 | File.open('data.yaml', 'w') { |out| 502 | out << "# This is a dudle poll file\n" 503 | out << to_yaml 504 | out.chmod(0o660) 505 | } 506 | VCS.commit(comment) 507 | 508 | lock.flock(File::LOCK_UN) 509 | end 510 | } 511 | end 512 | 513 | ############################### 514 | # comment related functions 515 | ############################### 516 | def add_comment(name, comment) 517 | @comment << [Time.now, CGI.escapeHTML(name.strip), CGI.escapeHTML(comment.strip).gsub("\r\n", '
    ')] 518 | store "Comment added by #{name}" 519 | end 520 | 521 | def delete_comment(deltime) 522 | @comment.each_with_index { |c, i| 523 | if c[0].strftime('%s') == deltime 524 | store "Comment from #{@comment.delete_at(i)[1]} deleted" 525 | end 526 | } 527 | end 528 | 529 | ############################### 530 | # column related functions 531 | ############################### 532 | def delete_column(column) 533 | return false unless @head.delete_column(column) 534 | 535 | store "Column #{column} deleted" 536 | true 537 | end 538 | 539 | def edit_column(oldcolumn, newtitle, cgi) 540 | parsedtitle = @head.edit_column(oldcolumn, newtitle, cgi) 541 | store "Column #{parsedtitle} #{oldcolumn == '' ? 'added' : 'edited'}" if parsedtitle 542 | end 543 | 544 | def edit_column_htmlform(activecolumn, revision) 545 | @head.edit_column_htmlform(activecolumn, revision) 546 | end 547 | end 548 | 549 | if __FILE__ == $0 550 | require 'test/unit' 551 | require 'cgi' 552 | require 'pp' 553 | 554 | SITE = 'glvhc_8nuv_8fchi09bb12a-23_uvc' 555 | class Poll 556 | attr_accessor :head, :data, :comment 557 | 558 | def store(comment); end 559 | end 560 | # ┌───────────────────┬─────────────────────────────────┬────────────┐ 561 | # │ │ May 2009 │ │ 562 | # ├───────────────────┼────────┬────────────────────────┼────────────┤ 563 | # │ │Tue, 05 │ Sat, 23 │ │ 564 | # ├───────────────────┼────────┼────────┬────────┬──────┼────────────┤ 565 | # │ Name ▾▴ │ ▾▴ │10:00 ▾▴│11:00 ▾▴│foo ▾▴│Last edit ▾▴│ 566 | # ├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ 567 | # │Alice ^✍ │✔ │✘ │✔ │✘ │24.11, 18:15│ 568 | # ├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ 569 | # │Bob ^✍ │✔ │✔ │✘ │? │24.11, 18:15│ 570 | # ├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ 571 | # │Dave ^✍ │✘ │? │✔ │✔ │24.11, 18:16│ 572 | # ├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ 573 | # │Carol ^✍ │✔ │✔ │? │✘ │24.11, 18:16│ 574 | # ├───────────────────┼────────┼────────┼────────┼──────┼────────────┤ 575 | # │total │3 │2 │2 │1 │ │ 576 | # └───────────────────┴────────┴────────┴────────┴──────┴────────────┘ 577 | 578 | class PollTest < Test::Unit::TestCase 579 | Y = Poll::YESVAL 580 | N = Poll::NOVAL 581 | M = Poll::MAYBEVAL 582 | A = 'Alice' 583 | B = 'Bob' 584 | C = 'Carol' 585 | D = 'Dave' 586 | Q = '2009-05-05' 587 | W = '2009-05-23 10:00' 588 | E = '2009-05-23 11:00' 589 | R = '2009-05-23 foo' 590 | def setup 591 | def add_participant(type, user, votearray) 592 | h = { Q => votearray[0], W => votearray[1], E => votearray[2], R => votearray[3] } 593 | @polls[type].add_participant('', user, h) 594 | end 595 | 596 | @polls = {} 597 | %w[time normal].each { |type| 598 | @polls[type] = Poll.new(SITE, type) 599 | 600 | @polls[type].edit_column('', '2009-05-05', { 'columndescription' => '' }) 601 | 2.times { |t| 602 | @polls[type].edit_column('', "2009-05-23 #{t + 10}:00", { 'columntime' => "#{t + 10}:00", 'columndescription' => '' }) 603 | } 604 | @polls[type].edit_column('', '2009-05-23 foo', { 'columntime' => 'foo', 'columndescription' => '' }) 605 | 606 | add_participant(type, A, [Y, N, Y, N]) 607 | add_participant(type, B, [Y, Y, N, M]) 608 | add_participant(type, D, [N, M, Y, Y]) 609 | add_participant(type, C, [Y, Y, M, N]) 610 | } 611 | end 612 | 613 | def test_sort 614 | %w[time normal].each { |type| 615 | comment = "Test Type: #{type}" 616 | assert_equal([A, B, C, D], @polls[type].sort_data(['name']).collect { |a| a[0] }, comment) 617 | assert_equal([A, B, D, C], @polls[type].sort_data(['timestamp']).collect { |a| a[0] }, comment) 618 | assert_equal([B, C, D, A], @polls[type].sort_data([W, 'name']).collect { |a| a[0] }, comment) 619 | assert_equal([B, A, C, D], @polls[type].sort_data([Q, R, E]).collect { |a| a[0] }, comment + ' ' + [Q, R, E].join('; ')) 620 | } 621 | end 622 | end 623 | 624 | class StringTest < Test::Unit::TestCase 625 | def test_htmlid 626 | assert_equal('foo.bar.', 'foo bar '.to_htmlID) 627 | assert_equal('foo.bar.', 'foo bar '.to_htmlID) 628 | assert_equal('foo.bar.0', 'foo.bar '.to_htmlID) 629 | assert_equal('foo.bar.00', 'foo.bar 0'.to_htmlID) 630 | assert_equal('foo.bar.', 'foo bar '.to_htmlID) 631 | assert_equal('foo.bar.1', 'foo bar.'.to_htmlID) 632 | assert_equal('foo.bar.2', 'foo?bar.'.to_htmlID) 633 | assert_equal('foo.bar.3', 'foo bar?'.to_htmlID) 634 | assert_equal('foo.bar.2', 'foo?bar.'.to_htmlID) 635 | end 636 | end 637 | end 638 | -------------------------------------------------------------------------------- /pollhead.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | class PollHead 21 | def initialize 22 | @data = {} 23 | end 24 | 25 | def col_size 26 | @data.size 27 | end 28 | 29 | # returns a sorted array of all columns 30 | # column should be the internal representation 31 | # column.to_s should deliver humanreadable form 32 | def columns 33 | @data.keys.sort 34 | end 35 | 36 | # column is in human readable form 37 | # returns true if deletion successful 38 | def delete_column(column) 39 | !@data.delete(column).nil? 40 | end 41 | 42 | # add new column if columnid = "" 43 | # returns parsed title or false if parsed title == "" 44 | def edit_column(column, newtitle, cgi) 45 | delete_column(column) if column != '' 46 | parsedtitle = newtitle.strip 47 | 48 | return false unless parsedtitle != '' 49 | 50 | @data[parsedtitle] = cgi['columndescription'].strip 51 | parsedtitle 52 | end 53 | 54 | def to_html(scols, showeditbuttons = false, activecolumn = nil) 55 | def sortsymb(scols, col) 56 | <#{scols.include?(col) ? _('Sort') : _('No Sort')} 58 | 59 | SORTSYMBOL 60 | end 61 | ret = '' 62 | ret += "" + _('Name') + " #{sortsymb(scols, 'name')}\n" unless showeditbuttons 63 | @data.sort.each { |columntitle, columndescription| 64 | ret += "" unless showeditbuttons 68 | ret += "#{CGI.escapeHTML(columntitle)} #{CGI.escapeHTML(columndescription)} " 69 | ret += "#{sortsymb(scols, columntitle)}" unless showeditbuttons 70 | if showeditbuttons 71 | editstr = _('Edit option') 72 | deletestr = _('Delete option') 73 | ret += < 75 |
    76 | 77 | #{EDIT} 78 | | 79 | 80 | 81 |
    82 | 83 | EDITDELETE 84 | end 85 | ret += '' 86 | } 87 | ret += "" + _('Last edit') + " #{sortsymb(scols, 'timestamp')}\n" unless showeditbuttons 88 | ret += "\n" 89 | ret 90 | end 91 | 92 | def edit_column_htmlform(activecolumn, revision) 93 | if activecolumn != '' 94 | title = activecolumn 95 | description = @data[title] 96 | title = CGI.escapeHTML(title) 97 | hiddeninput = "" 98 | end 99 | columntitlestr = _('Option') 100 | descriptionstr = _('Description (optional)') 101 | addeditstr = _('Confirm option') 102 | previewstr = _('Preview') 103 | hint = _('Enter all the options (columns) which you want the participants of the poll to choose among. For each option you give here, the participants will choose a vote.') 104 | ret = < 106 |
    #{hint}
    107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 121 | 122 |
    117 | 118 | #{hiddeninput} 119 | 120 |
    123 | 124 | END 125 | if col_size > 0 126 | ret += <#{previewstr} 128 | 129 | #{to_html([], true, activecolumn)} 130 |
    131 | END 132 | end 133 | ret 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /print.css: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright 2009-2019 Benjamin Kellermann * 3 | * * 4 | * This file is part of Dudle. * 5 | * * 6 | * Dudle is free software: you can redistribute it and/or modify it under * 7 | * the terms of the GNU Affero General Public License as published by * 8 | * the Free Software Foundation, either version 3 of the License, or * 9 | * (at your option) any later version. * 10 | * * 11 | * Dudle is distributed in the hope that it will be useful, but WITHOUT ANY * 12 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or * 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * 14 | * License for more details. * 15 | * * 16 | * You should have received a copy of the GNU Affero General Public License * 17 | * along with dudle. If not, see . * 18 | ***************************************************************************/ 19 | 20 | @page { 21 | size: landscape; 22 | } 23 | 24 | .invisible { 25 | visibility: hidden; 26 | } 27 | .header:after, .headerSort:after { 28 | content: "" !important; 29 | } 30 | th.invisible, 31 | td.invisible{ 32 | border-style: none; 33 | } 34 | 35 | tr.participantrow td:first-child { 36 | border-right-style: none; 37 | } 38 | tr.participantrow td.name { 39 | border-left-style: none; 40 | } 41 | 42 | 43 | td.match_100{ background-color: #99cc66 !important; } 44 | td.match_90 { background-color: #add666 !important; } 45 | td.match_80 { background-color: #c1e066 !important; } 46 | td.match_70 { background-color: #d6ea66 !important; } 47 | td.match_60 { background-color: #eaf466 !important; } 48 | td.match_50 { background-color: #ffff66 !important; } 49 | td.match_40 { background-color: #FFea66 !important; } 50 | td.match_30 { background-color: #FFd666 !important; } 51 | td.match_20 { background-color: #FFc166 !important; } 52 | td.match_10 { background-color: #FFad66 !important; } 53 | td.match_0 { background-color: #FF9966 !important; } 54 | 55 | td.a_yes__, input.chosen { background-color:#9c6; } 56 | td.c_no___ { background-color:#F96; } 57 | td.b_maybe { background-color:#FF6; } 58 | td.undecided { background-color:#DDD;color:#666 } 59 | 60 | td.name { 61 | text-align:right; 62 | } 63 | 64 | td.name a,td.name a:visited { 65 | text-decoration: none; 66 | color: black; 67 | } 68 | 69 | #active { 70 | background-color:#A00; 71 | } 72 | 73 | th.weekday{ 74 | width: 2.5em; 75 | } 76 | 77 | input.navigation, input.disabled, input.chosen, input.notchosen { 78 | border-width: 1px; 79 | border-style: solid; 80 | border-color: black; 81 | padding: 0px; 82 | width: 100%; 83 | height: 100%; 84 | } 85 | 86 | input.navigation { 87 | color: white; 88 | background-color: black; 89 | } 90 | 91 | input.disabled { background-color:#DDD;color:#BBB} 92 | td.calendarday{ 93 | border: 1px; 94 | padding: 0px; 95 | margin: 0px; 96 | } 97 | 98 | table { 99 | border: none; 100 | border-spacing: 0; 101 | border-collapse: collapse; 102 | } 103 | 104 | td.create_poll{ 105 | text-align:left; 106 | } 107 | 108 | td { 109 | vertical-align:middle; 110 | text-align:center; 111 | padding: 1px 3px; 112 | border-width: 1px; 113 | border-style: dotted; 114 | } 115 | 116 | td.polls { 117 | text-align:left; 118 | } 119 | 120 | div#add_comment,div#edit_column, div#history, div#backlink, input.delete_comment_button, tr#add_participant, div#configlink, div#tabs, p#history, span.edituser, span.sortsymb, div#languageChooser, input[type=submit], div#breadcrumbs, a.comment_sort, a.top_bottom_ref, .visually-hidden { 121 | display: none; 122 | } 123 | 124 | th { 125 | padding: 1px 3px; 126 | font-weight: normal; 127 | border-width: 1px; 128 | border-style: dotted; 129 | } 130 | 131 | th a{ 132 | text-decoration: none; 133 | color: black; 134 | } 135 | 136 | td.date { 137 | text-align: left; 138 | } 139 | 140 | 141 | pre#configwarning { 142 | font-family:"Courier New",Courier,monospace; 143 | letter-spacing:0; 144 | margin-top: -12ex; 145 | line-height:95%; 146 | margin-left: -2ex; 147 | } 148 | 149 | 150 | h1 { 151 | text-align: center; 152 | } 153 | 154 | 155 | div.comment { margin-top: 1ex; } 156 | -------------------------------------------------------------------------------- /timepollhead.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | # BUGFIX for Time.parse, which handles the zone indeterministically 21 | class << Time 22 | alias old_parse parse 23 | def Time.parse(date, _now = now) 24 | Time.old_parse('2009-10-25 00:30') 25 | Time.old_parse(date) 26 | end 27 | end 28 | 29 | require_relative 'timestring' 30 | 31 | class TimePollHead 32 | def initialize 33 | @data = [] 34 | end 35 | 36 | def col_size 37 | @data.size 38 | end 39 | 40 | # returns a sorted array of all columns 41 | # column should be the internal representation 42 | # column.to_s should deliver humanreadable form 43 | def columns 44 | @data.sort.collect { |day| day.to_s } 45 | end 46 | 47 | def concrete_times 48 | h = {} 49 | @data.each { |ds| h[ds.time_to_s] = true } 50 | h.keys 51 | end 52 | 53 | def date_included?(date) 54 | ret = false 55 | @data.each { |ds| 56 | ret ||= ds.date == date 57 | } 58 | ret 59 | end 60 | 61 | # deletes one concrete column 62 | # adds the empty day, if it was the last concrete time of the day 63 | # returns true if deletion successful 64 | def delete_concrete_column(col) 65 | ret = !@data.delete(col).nil? 66 | @data << TimeString.new(col.date, nil) unless date_included?(col.date) 67 | ret 68 | end 69 | 70 | # column is in human readable form 71 | # returns true if deletion successful 72 | def delete_column(column) 73 | if $cgi.include?('togglealloff') # delete one time 74 | head_count('%Y-%m-%d', false).each { |day, _num| 75 | delete_concrete_column(TimeString.new(day, column)) 76 | } 77 | return true 78 | end 79 | col = TimeString.from_s(column) 80 | return delete_concrete_column(col) if col.time 81 | 82 | # delete all concrete times on the given day 83 | deldata = [] 84 | @data.each { |ts| 85 | deldata << ts if ts.date == col.date 86 | } 87 | deldata.each { |ts| 88 | @data.delete(ts) 89 | } 90 | !deldata.empty? 91 | end 92 | 93 | def parsecolumntitle(title) 94 | if $cgi.include?('add_remove_column_day') 95 | parsed_date = YAML.load(Time.parse("#{$cgi['add_remove_column_month']}-#{$cgi['add_remove_column_day']} #{title}").to_yaml) 96 | else 97 | earlytime = @head.keys.collect { |t| t.strftime('%H:%M') }.sort[0] 98 | parsed_date = YAML.load(Time.parse("#{$cgi['add_remove_column_month']}-#{title} #{earlytime}").to_yaml) 99 | end 100 | parsed_date 101 | end 102 | 103 | # returns parsed title or nil in case of column not changed 104 | def edit_column(column, newtitle, cgi) 105 | if cgi.include?('toggleallon') 106 | head_count('%Y-%m-%d', false).each { |day, _num| 107 | parsed_date = TimeString.new(day, newtitle) 108 | delete_column(day) if @data.include?(TimeString.new(day, nil)) 109 | @data << parsed_date unless @data.include?(parsed_date) 110 | } 111 | return newtitle 112 | end 113 | if cgi.include?('columntime') && cgi['columntime'] == '' 114 | @edit_column_error = _('To add a time other than the default times, please enter some string here (e. g., 09:30, morning, afternoon).') 115 | return nil 116 | end 117 | delete_column(column) if column != '' 118 | parsed_date = TimeString.new(newtitle, cgi['columntime'] == '' ? nil : cgi['columntime']) 119 | if @data.include?(parsed_date) 120 | @edit_column_error = _('This time has already been selected.') 121 | nil 122 | else 123 | @data << parsed_date 124 | parsed_date.to_s 125 | end 126 | end 127 | 128 | # returns a sorted array, containing the big units and how often each small is in the big one 129 | # small and big must be formatted for strftime 130 | # ex: head_count("%Y-%m") returns an array like [["2009-03",2],["2009-04",3]] 131 | # if notime = true, the time field is stripped out before counting 132 | def head_count(elem, notime) 133 | data = @data.collect { |day| day.date } 134 | data.uniq! if notime 135 | ret = Hash.new(0) 136 | data.each { |day| 137 | ret[day.strftime(elem)] += 1 138 | } 139 | ret.sort 140 | end 141 | 142 | def to_html(scols, _config = false, _activecolumn = nil) 143 | ret = "" 144 | head_count('%Y-%m', false).each { |title, count| 145 | year, month = title.split('-').collect { |e| e.to_i } 146 | ret += "#{Date.parse("#{year}-#{month}-01").strftime('%b %Y')}\n" 147 | } 148 | 149 | ret += "" 150 | head_count('%Y-%m-%d', false).each { |title, count| 151 | ret += "#{Date.parse(title).strftime('%a, %d')}\n" 152 | } 153 | 154 | def sortsymb(scols, col) 155 | <<-SORTSYMBOL 156 | #{scols.include?(col) ? _('Sort') : _('No Sort')} 157 | 158 | SORTSYMBOL 159 | end 160 | 161 | ret += "" + _('Name') + " #{sortsymb(scols, 'name')}" 162 | @data.sort.each { |date| 163 | ret += "#{CGI.escapeHTML(date.time_to_s)} #{sortsymb(scols, date.to_s)}\n" 164 | } 165 | ret += "" + _('Last edit') + " #{sortsymb(scols, 'timestamp')}\n\n" 166 | ret 167 | end 168 | 169 | def datenavi(val, revision) 170 | case val 171 | when MONTHBACK 172 | navimonth = Date.parse("#{@startdate.strftime('%Y-%m')}-1") - 1 173 | navimonthDescription = _('Navigate one month back') 174 | when MONTHFORWARD 175 | navimonth = Date.parse("#{@startdate.strftime('%Y-%m')}-1") + 31 176 | navimonthDescription = _('Navigate one month forward') 177 | else 178 | raise "Unknown navi value #{val}" 179 | end 180 | < 182 |
    183 |
    184 | 185 | 186 | 187 | 188 | 189 |
    190 |
    191 | 192 | END 193 | end 194 | 195 | def timenavi(val, revision) 196 | case val 197 | when EARLIER 198 | return '' if @firsttime == 0 199 | 200 | str = EARLIER + ' ' + _('Earlier') 201 | strAria = _('Add previous two hours') 202 | firsttime = [@firsttime - 2, 0].max 203 | lasttime = @lasttime 204 | when LATER 205 | return '' if @lasttime == 23 206 | 207 | str = LATER + ' ' + _('Later') 208 | strAria = _('Add following two hours') 209 | firsttime = @firsttime 210 | lasttime = [@lasttime + 2, 23].min 211 | else 212 | raise "Unknown navi value #{val}" 213 | end 214 | < 216 | 217 |
    218 |
    219 | 220 | 221 | 222 | 223 | 224 |
    225 |
    226 | 227 | 228 | END 229 | end 230 | 231 | def edit_column_htmlform(_activecolumn, revision) 232 | # calculate start date, first and last time to show 233 | if $cgi.include?('add_remove_column_month') 234 | @startdate = Date.parse("#{$cgi['add_remove_column_month']}-1") 235 | else 236 | @startdate = Date.parse("#{Date.today.year}-#{Date.today.month}-1") 237 | end 238 | 239 | times = concrete_times 240 | times.delete('') # do not display empty cell in edit-column-form 241 | realtimes = times.collect { |t| 242 | begin 243 | Time.parse(t) if t =~ /^\d\d:\d\d$/ 244 | rescue ArgumentError 245 | end 246 | }.compact 247 | [9, 16].each { |i| realtimes << Time.parse("#{i.to_s.rjust(2, '0')}:00") } 248 | 249 | %w[firsttime lasttime].each { |t| 250 | realtimes << Time.parse($cgi[t]) if $cgi.include?(t) 251 | } 252 | 253 | @firsttime = realtimes.min.strftime('%H').to_i 254 | @lasttime = realtimes.max.strftime('%H').to_i 255 | 256 | def add_remove_button(klasse, buttonlabel, action, columnstring, revision, pretext = '', _arialabel = columnstring, properdate) 257 | if %w[chosen delete].include?(klasse) 258 | titlestr = format(_('Delete the column %s'), DATE: CGI.escapeHTML(properdate)) 259 | if klasse == 'delete' 260 | klasse += ' headerSymbol' 261 | end 262 | elsif klasse == 'disabled' 263 | titlestr = format(_('Add the already past column %s'), DATE: CGI.escapeHTML(properdate)) 264 | else 265 | titlestr = format(_('Add the column %s'), DATE: CGI.escapeHTML(properdate)) 266 | end 267 | <
    269 |
    270 | #{pretext} 271 | 272 | 273 | 274 | 275 | 276 |
    277 |
    278 | FORM 279 | end 280 | 281 | hintstr = _('Click on the dates to add or remove columns.') 282 | ret = < 284 |
    285 | #{hintstr} 286 |
    287 | 288 | END 289 | ret += datenavi(MONTHBACK, revision) 290 | ret += "" 291 | ret += datenavi(MONTHFORWARD, revision) 292 | 293 | ret += "\n" 294 | 295 | 7.times { |i| 296 | # 2010-03-01 was a Monday, so we can use this month for a dirty hack 297 | ret += "" 298 | } 299 | ret += "\n" 300 | 301 | ((@startdate.wday + 7 - 1) % 7).times { 302 | ret += "" 303 | } 304 | d = @startdate 305 | while true 306 | klasse = 'notchosen' 307 | varname = 'new_columnname' 308 | klasse = 'disabled' if d < Date.today 309 | if date_included?(d) 310 | klasse = 'chosen' 311 | varname = 'deletecolumn' 312 | end 313 | ret += "" 314 | d = d.next 315 | break if d.month != @startdate.month 316 | 317 | ret += "\n" if d.wday == 1 318 | end 319 | _('added') 320 | _('removed') 321 | ret += <
    #{@startdate.strftime('%b %Y')}
    #{Date.parse("2010-03-0#{i + 1}").strftime('%a')}
    #{add_remove_button(klasse, d.day, varname, d.strftime('%Y-%m-%d'), revision, d.strftime('%d-%m-%Y'))}
    323 |
    324 | 325 | END 326 | 327 | ########################### 328 | # starting hour input 329 | ########################### 330 | ret += "" 331 | if col_size > 0 332 | optstr = _('Optional:') 333 | hintstr = _('Select specific start times.') 334 | ret += < 336 | #{optstr}
    337 | #{hintstr} 338 |
    339 | 340 | 341 | 342 | END 343 | 344 | ret += "" 345 | head_count('%Y-%m', true).each { |title, count| 346 | year, month = title.split('-').collect { |e| e.to_i } 347 | ret += "\n" 348 | } 349 | 350 | ret += "' 351 | 352 | head_count('%Y-%m-%d', true).each { |title, _count| 353 | coltime = Date.parse(title) 354 | ret += '' 355 | } 356 | 357 | ret += '' 358 | 359 | days = @data.sort.collect { |date| date.date }.uniq 360 | 361 | chosenstr = { 362 | 'chosen' => _('Selected'), 363 | 'notchosen' => _('Not selected'), 364 | 'disabled' => _('Past') 365 | } 366 | 367 | ret += timenavi(EARLIER, revision) 368 | 369 | (@firsttime..@lasttime).each { |i| times << "#{i.to_s.rjust(2, '0')}:00" } 370 | times.flatten.compact.uniq.sort { |a, b| 371 | if a =~ /^\d\d:\d\d$/ && !(b =~ /^\d\d:\d\d$/) 372 | -1 373 | elsif !(a =~ /^\d\d:\d\d$/) && b =~ /^\d\d:\d\d$/ 374 | 1 375 | else 376 | a.to_i == b.to_i ? a <=> b : a.to_i <=> b.to_i 377 | end 378 | }.each { |time| 379 | ret += < 381 | 382 | 409 | END 410 | days.each { |day| 411 | timestamp = TimeString.new(day, time) 412 | klasse = 'notchosen' 413 | klasse = 'disabled' if timestamp < TimeString.now 414 | 415 | if @data.include?(timestamp) 416 | klasse = 'chosen' 417 | hiddenvars = "" 418 | else 419 | hiddenvars = "" 420 | if @data.include?(TimeString.new(day, nil)) # change day instead of removing it if no specific hour exists for this day 421 | hiddenvars += "" 422 | end 423 | end 424 | ret += '' 425 | } 426 | ret += "\n" 427 | } 428 | ret += timenavi(LATER, revision) 429 | 430 | ret += "" 431 | days.each { |d| 432 | ret += < 434 |
    435 |
    436 | 437 | 438 | 439 | 440 | 441 | END 442 | if @data.include?(TimeString.new(d, nil)) 443 | ret += "" 444 | end 445 | addstr = _('Add') 446 | hintstr = _('e. g., 09:30, morning, afternoon') 447 | timestr = _('Write a specific time for ') 448 | ret += <
    450 | 451 |
    452 | 453 | 454 | END 455 | } 456 | 457 | ret += '
    #{Date.parse("#{year}-#{month}-01").strftime('%b %Y')}
    " + _('Time') + '' + add_remove_button('delete', DELETE, 'deletecolumn', coltime.strftime('%Y-%m-%d'), revision, "#{coltime.strftime('%a, %d')} ", coltime.strftime('%d-%m-%Y')) + '
    ' + add_remove_button(klasse, chosenstr[klasse], 'columntime', CGI.escapeHTML(timestamp.time_to_s), revision, hiddenvars, timestamp.to_s) + '
    ' 458 | ret += "
    #{@edit_column_error}
    " if @edit_column_error 459 | end 460 | ret += '' 461 | ret 462 | end 463 | end 464 | -------------------------------------------------------------------------------- /timestring.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | require 'date' 21 | require 'time' 22 | 23 | class TimeString 24 | attr_reader :date, :time 25 | 26 | def initialize(date, time) 27 | @date = date.class == Date ? date : Date.parse(date) 28 | if time =~ /^\d[\d]?:\d[\d]?$/ 29 | begin 30 | # TODO: what to do with 24:00 ??? 31 | # if time == "24:00" 32 | # @date += 1 33 | # time = "00:00" 34 | # end 35 | @time = Time.parse("#{@date} #{time}") 36 | rescue ArgumentError 37 | @time = time 38 | end 39 | else 40 | @time = time 41 | end 42 | end 43 | 44 | def self.from_s(string) 45 | date = string.scan(/^(\d\d\d\d-\d\d-\d\d).*$/).flatten[0] 46 | time = string.scan(/^\d\d\d\d-\d\d-\d\d (.*)$/).flatten[0] 47 | TimeString.new(date, time) 48 | end 49 | 50 | def self.now 51 | TimeString.new(Date.today, Time.now) 52 | end 53 | include Comparable 54 | def equal?(other) 55 | self <=> other 56 | end 57 | 58 | def eql?(other) 59 | self <=> other 60 | end 61 | 62 | def <=>(other) 63 | return -1 if other.class != TimeString 64 | 65 | if date == other.date 66 | if time.class == String && other.time.class == String 67 | time.to_i == other.time.to_i ? time <=> other.time : time.to_i <=> other.time.to_i 68 | elsif time.class == Time && other.time.class == Time 69 | time <=> other.time 70 | elsif time.class == NilClass && other.time.class == NilClass 71 | 0 72 | else 73 | time.class == String ? 1 : -1 74 | end 75 | else 76 | date <=> other.date 77 | end 78 | end 79 | 80 | def to_s 81 | if @time 82 | "#{CGI.escapeHTML(@date.to_s)} #{time_to_s}" 83 | else 84 | CGI.escapeHTML(@date.to_s) 85 | end 86 | end 87 | 88 | def inspect 89 | "TS: date: #{@date} time: #{@time ? time_to_s : 'nil'}" 90 | end 91 | 92 | def time_to_s 93 | return time.strftime('%H:%M') if @time.class == Time 94 | 95 | @time.to_s 96 | end 97 | end 98 | 99 | if __FILE__ == $0 100 | require 'test/unit' 101 | require 'pp' 102 | 103 | class TimeStringTest < Test::Unit::TestCase 104 | def test_uniq 105 | a = TimeString.new('2010-01-22', '1:00') 106 | b = TimeString.new('2010-01-22', '1:00') 107 | assert(a == b) 108 | assert(a === b) 109 | assert(a.equal?(b)) 110 | assert(a.eql?(b)) 111 | assert([a].include?(b)) 112 | assert_equal([a], [a, b].uniq) 113 | end 114 | end 115 | 116 | end 117 | -------------------------------------------------------------------------------- /vcs_git.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | require 'time' 21 | require_relative 'log' 22 | require 'open3' 23 | require 'tempfile' 24 | 25 | def runcmd *args 26 | Open3.popen3(*args) { |_i, o, _e, _t| o.read } 27 | end 28 | 29 | class VCS 30 | GITCMD = 'git' 31 | def self.init 32 | runcmd(GITCMD, 'init') 33 | end 34 | 35 | def self.rm(file) 36 | runcmd(GITCMD, 'rm', file) 37 | end 38 | 39 | def self.add(file) 40 | runcmd(GITCMD, 'add', file) 41 | end 42 | 43 | def self.revno 44 | # there is a bug in git log --format, which suppresses the \n on the last line 45 | runcmd(GITCMD, 'log', '--format=format:x').scan("\n").size + 1 46 | end 47 | 48 | def self.cat(revision, file) 49 | revs = runcmd(GITCMD, 'log', '--format=format:%H').split("\n").reverse 50 | runcmd(GITCMD, 'show', "#{revs[revision - 1]}:#{file}") 51 | end 52 | 53 | def self.history 54 | log = runcmd(GITCMD, 'log', "--format=format:%s\t%ai").force_encoding('utf-8').split("\n").reverse 55 | ret = Log.new 56 | log.each_with_index { |s, i| 57 | a = s.scan(/^([^\t]*)(.*)$/).flatten 58 | ret.add(i + 1, Time.parse(a[1]), a[0]) 59 | } 60 | ret 61 | end 62 | 63 | def self.commit(comment) 64 | tmpfile = Tempfile.new('commit') 65 | tmpfile.write(comment) 66 | tmpfile.close 67 | ret = runcmd(GITCMD, 'commit', '-a', '-F', tmpfile.path) 68 | tmpfile.unlink 69 | ret 70 | end 71 | 72 | def self.branch(source, target) 73 | runcmd(GITCMD, 'clone', source, target) 74 | end 75 | 76 | def self.revert(revno) 77 | revhash = runcmd(GITCMD, 'log', '--format=%H').split("\n").reverse[revno - 1] 78 | runcmd(GITCMD, 'checkout', revhash, '.') 79 | VCS.commit("Reverted Poll to version #{revno}") 80 | end 81 | 82 | def self.reset(revno) 83 | revhash = runcmd(GITCMD, 'log', '--format=%H').split("\n").reverse[revno - 1] 84 | runcmd(GITCMD, 'checkout', '-B', 'master', revhash) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /vcs_test.rb: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # Copyright 2009-2019 Benjamin Kellermann # 3 | # # 4 | # This file is part of Dudle. # 5 | # # 6 | # Dudle is free software: you can redistribute it and/or modify it under # 7 | # the terms of the GNU Affero General Public License as published by # 8 | # the Free Software Foundation, either version 3 of the License, or # 9 | # (at your option) any later version. # 10 | # # 11 | # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # 12 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or # 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # 14 | # License for more details. # 15 | # # 16 | # You should have received a copy of the GNU Affero General Public License # 17 | # along with dudle. If not, see . # 18 | ############################################################################ 19 | 20 | if __FILE__ == $0 21 | require 'test/unit' 22 | require 'pp' 23 | unless ARGV[0] 24 | puts "Usage: ruby #{$0} git" 25 | exit 26 | end 27 | require "vcs_#{ARGV[0]}" 28 | require 'benchmark' 29 | 30 | class VCS_test < Test::Unit::TestCase 31 | def setup 32 | @data = %w[foo bar baz spam ham egg] 33 | @history = %w[aaa bbb ccc ddd eee fff] 34 | @repo = "/tmp/vcs_test_#{rand(10_000)}" 35 | Dir.mkdir(@repo) 36 | Dir.chdir(@repo) 37 | VCS.init 38 | File.open('data.txt', 'w').close 39 | VCS.add('data.txt') 40 | @data.each_index { |i| 41 | File.open('data.txt', 'w') { |f| f << @data[i] } 42 | VCS.commit(@history[i]) 43 | } 44 | @b = 0 45 | @t = '' 46 | end 47 | 48 | def teardown 49 | puts @repo 50 | # Dir.chdir("/") 51 | # %x{rm -rf #{@repo}} 52 | puts "#{@t}: #{@b}" 53 | end 54 | 55 | def test_cat 56 | @data.each_with_index { |item, revnominusone| 57 | result = '' 58 | @b += Benchmark.measure { 59 | result = VCS.cat(revnominusone + 1, 'data.txt') 60 | }.total 61 | assert_equal(item, result, "revno: #{revnominusone + 1}") 62 | } 63 | @t = 'cat' 64 | end 65 | 66 | def test_revno 67 | r = -1 68 | @b += Benchmark.measure { 69 | r = VCS.revno 70 | }.total 71 | assert_equal(@data.size, r) 72 | @t = 'revno' 73 | end 74 | 75 | def test_history 76 | l = nil 77 | @b += Benchmark.measure { 78 | l = VCS.history 79 | }.total 80 | pp l 81 | exit 82 | assert_equal(@data.size, l.size) 83 | @history.each_with_index { |h, revminusone| 84 | assert_equal(h, l[revminusone + 1].comment) 85 | } 86 | 87 | @t = 'history' 88 | end 89 | end 90 | end 91 | --------------------------------------------------------------------------------