Compare commits

...

9 commits

Author SHA1 Message Date
eb835bcebe Translate email subject 2026-01-13 20:07:17 -06:00
382c6d6831 Update Catalan translation 2026-01-13 20:07:17 -06:00
9b5155a651 Fix time zone when spoofing user 2026-01-13 20:07:17 -06:00
cdf4c2d0c7 Avoid table to grow too big 2026-01-13 20:07:17 -06:00
ffb23d3751 Display number of votes 2026-01-13 20:07:17 -06:00
1066e269af Fix English 2026-01-13 20:07:17 -06:00
fdc2d53997 Redesign results page
- Replace chart with table
- Analyze tiebreak
- Merge all info in a single list of cards

Closes #25
Closes #13
2026-01-13 20:04:21 -06:00
d429c16a9e Add tiebreak analyzer 2026-01-10 19:00:26 -06:00
08fe4163bc Initialize a counter for all possible values 2026-01-09 19:52:16 -06:00
13 changed files with 4869 additions and 26788 deletions

View file

@ -10,6 +10,5 @@ gem 'base64', '0.1.1'
gem 'forwardable', '1.3.2' gem 'forwardable', '1.3.2'
gem 'bcrypt' gem 'bcrypt'
gem 'gettext' gem 'gettext'
gem 'chartkick'
gem 'mail' gem 'mail'
gem 'redcarpet' gem 'redcarpet'

View file

@ -23,7 +23,6 @@ GEM
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0) benchmark (0.4.0)
bigdecimal (3.1.9) bigdecimal (3.1.9)
chartkick (5.1.4)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.5)
connection_pool (2.5.0) connection_pool (2.5.0)
date (3.4.1) date (3.4.1)
@ -108,7 +107,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
base64 (= 0.1.1) base64 (= 0.1.1)
bcrypt bcrypt
chartkick
forwardable (= 1.3.2) forwardable (= 1.3.2)
gettext gettext
mail mail

20
mj.rb
View file

@ -13,6 +13,9 @@ class MajorityJudgment
if arg.is_a?(Array) if arg.is_a?(Array)
@ratings = arg @ratings = arg
@count = { } @count = { }
self.class.values.each do |v|
@count[v[:id]] = 0
end
@n = 0 @n = 0
ratings.each do |r| ratings.each do |r|
@count.has_key?(r) ? @count[r] = @count[r] + 1 : @count[r] = 1 @count.has_key?(r) ? @count[r] = @count[r] + 1 : @count[r] = 1
@ -62,6 +65,23 @@ class MajorityJudgment
return MajorityJudgment.new(counta) <=> MajorityJudgment.new(countb) return MajorityJudgment.new(counta) <=> MajorityJudgment.new(countb)
end end
def break_tie(other)
rounds = [ ]
s = MajorityJudgment.new(self.count.dup)
o = MajorityJudgment.new(other.count.dup)
rounds << [s, o]
while s.mj == o.mj and not s.mj == 0
new_s_count = s.count.dup
new_o_count = o.count.dup
new_s_count[s.mj] = new_s_count[s.mj] - 1
new_o_count[o.mj] = new_o_count[o.mj] - 1
s = MajorityJudgment.new(new_s_count)
o = MajorityJudgment.new(new_o_count)
rounds << [s, o]
end
return rounds
end
def to_s def to_s
puts "#{@count}: {M=>#{self.mj}, P=>#{self.proponents}, O=>#{self.opponents}}" puts "#{@count}: {M=>#{self.mj}, P=>#{self.proponents}, O=>#{self.opponents}}"
end end

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-04 17:25-0600\n" "POT-Creation-Date: 2026-01-13 20:00-0600\n"
"PO-Revision-Date: 2025-03-29 20:41-0600\n" "PO-Revision-Date: 2025-03-29 20:41-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -41,16 +41,20 @@ msgstr "Bé"
msgid "Very good" msgid "Very good"
msgstr "Molt bé" msgstr "Molt bé"
#: ../vedia.rb:125 #: ../vedia.rb:143
msgid "Incorrect email or password." msgid "Incorrect email or password."
msgstr "Correu o contrasenya incorrecte." msgstr "Correu o contrasenya incorrecte."
#: ../vedia.rb:147 #: ../vedia.rb:170
msgid "Reset your password" msgid "Reset your password"
msgstr "Reiniciar contrasenya" msgstr "Reiniciar contrasenya"
#: ../vedia.rb:582
msgid "Results of the vote: %{t}"
msgstr "Resultats de la votació: %{t}"
#: ../views/admin.erb:1 ../views/admin_users.erb:1 ../views/admin_votes.erb:1 #: ../views/admin.erb:1 ../views/admin_users.erb:1 ../views/admin_votes.erb:1
#: ../views/layout.erb:35 #: ../views/layout.erb:30
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
@ -58,7 +62,7 @@ msgstr "Admin"
msgid "Users" msgid "Users"
msgstr "Usuàries" msgstr "Usuàries"
#: ../views/admin.erb:13 ../views/layout.erb:31 ../views/votes.erb:1 #: ../views/admin.erb:13 ../views/layout.erb:26 ../views/votes.erb:1
msgid "Votes" msgid "Votes"
msgstr "Votacions" msgstr "Votacions"
@ -110,7 +114,7 @@ msgid "No vote organized."
msgstr "Cap votació organitzada." msgstr "Cap votació organitzada."
#: ../views/admin_users.erb:30 ../views/admin_votes.erb:38 #: ../views/admin_users.erb:30 ../views/admin_votes.erb:38
#: ../views/votes_show_closed.erb:89 #: ../views/votes_show_closed.erb:144
msgid "Ratings" msgid "Ratings"
msgstr "Valoracions" msgstr "Valoracions"
@ -147,8 +151,8 @@ msgid "State: %{state}"
msgstr "Estat: %{state}" msgstr "Estat: %{state}"
#: ../views/admin_votes.erb:19 ../views/votes_edit.erb:90 #: ../views/admin_votes.erb:19 ../views/votes_edit.erb:90
#: ../views/votes_show_closed.erb:123 ../views/votes_show_draft.erb:29 #: ../views/votes_show_closed.erb:178 ../views/votes_show_draft.erb:29
#: ../views/votes_show_open.erb:79 #: ../views/votes_show_open.erb:88
msgid "Organizers" msgid "Organizers"
msgstr "Organitzadores" msgstr "Organitzadores"
@ -162,7 +166,7 @@ msgstr "Suprimir la votació"
#: ../views/candidates_edit.erb:1 #: ../views/candidates_edit.erb:1
msgid "Edit candidate" msgid "Edit candidate"
msgstr "Edita la opció" msgstr "Edita l'opció"
#: ../views/candidates_edit.erb:6 ../views/votes_edit.erb:48 #: ../views/candidates_edit.erb:6 ../views/votes_edit.erb:48
msgid "Enter a name." msgid "Enter a name."
@ -183,40 +187,48 @@ msgid ""
"You can use <a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a>" "You can use <a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a>"
"." "."
msgstr "" msgstr ""
"Pots fer servir <a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a>." "Pots fer servir <a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown"
"</a>."
#: ../views/candidates_edit.erb:21 ../views/votes_edit_description.erb:21 #: ../views/candidates_edit.erb:21 ../views/votes_edit_description.erb:21
msgid "Save" msgid "Save"
msgstr "Guardar" msgstr "Guardar"
#: ../views/layout.erb:40 #: ../views/layout.erb:35
msgid "Logout" msgid "Logout"
msgstr "Desconnexió" msgstr "Desconnexió"
#: ../views/login.erb:1 ../views/login.erb:16 #: ../views/login.erb:1 ../views/login.erb:21
#: ../views/votes_show_unauthenticated.erb:16
msgid "Login" msgid "Login"
msgstr "Connexió" msgstr "Connexió"
#: ../views/login.erb:9 ../views/reset.erb:5 ../views/reset_change.erb:13 #: ../views/login.erb:6 ../views/reset.erb:1 ../views/reset.erb:16
#: ../views/signup.erb:24 ../views/votes_edit.erb:102
#: ../views/votes_show_closed.erb:137 ../views/votes_show_open.erb:95
msgid "Email"
msgstr "Correu"
#: ../views/login.erb:13 ../views/reset_change.erb:17 ../views/signup.erb:28
msgid "Password"
msgstr "Contrasenya"
#: ../views/login.erb:18 ../views/signup.erb:1 ../views/signup.erb:31
msgid "Create account"
msgstr "Crear un compte"
#: ../views/login.erb:19 ../views/reset.erb:1 ../views/reset.erb:8
#: ../views/reset_change.erb:1 ../views/reset_change.erb:20 #: ../views/reset_change.erb:1 ../views/reset_change.erb:20
#: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1 #: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1
msgid "Reset password" msgid "Reset password"
msgstr "Reiniciar contrasenya" msgstr "Reiniciar contrasenya"
#: ../views/login.erb:12 ../views/reset.erb:13 ../views/reset_change.erb:13
#: ../views/signup.erb:24 ../views/votes_edit.erb:102
#: ../views/votes_show_closed.erb:192 ../views/votes_show_open.erb:104
#: ../views/votes_show_unauthenticated.erb:9
msgid "Email"
msgstr "Correu"
#: ../views/login.erb:16 ../views/reset_change.erb:17 ../views/signup.erb:28
#: ../views/votes_show_unauthenticated.erb:13
msgid "Password"
msgstr "Contrasenya"
#: ../views/login.erb:23 ../views/signup.erb:1 ../views/signup.erb:32
msgid "Create account"
msgstr "Crear un compte"
#: ../views/reset.erb:6
msgid "Enter an email address."
msgstr "Entra una direcció de correu."
#: ../views/reset_change.erb:6 ../views/signup.erb:17 #: ../views/reset_change.erb:6 ../views/signup.erb:17
msgid "Enter a password." msgid "Enter a password."
msgstr "Entra una contrasenya." msgstr "Entra una contrasenya."
@ -253,12 +265,20 @@ msgstr "El correu no és una direcció de correu vàlida."
msgid "An account already exists for %{email}." msgid "An account already exists for %{email}."
msgstr "Un compte ja existeix pel correu %{email}." msgstr "Un compte ja existeix pel correu %{email}."
#: ../views/votes.erb:18 ../views/votes_edit.erb:6 #: ../views/votes.erb:18 ../views/votes_show_open.erb:7
msgid "Voted"
msgstr "Votat"
#: ../views/votes.erb:20 ../views/votes_show_open.erb:9
msgid "Not voted"
msgstr "Sense votar"
#: ../views/votes.erb:23 ../views/votes_edit.erb:6
#: ../views/votes_show_closed.erb:7 #: ../views/votes_show_closed.erb:7
msgid "Organizer" msgid "Organizer"
msgstr "Organitzadora" msgstr "Organitzadora"
#: ../views/votes.erb:28 ../views/votes_new.erb:21 #: ../views/votes.erb:33 ../views/votes_new.erb:21
msgid "Create new vote" msgid "Create new vote"
msgstr "Crear una nova votació" msgstr "Crear una nova votació"
@ -275,8 +295,7 @@ msgid "Edit"
msgstr "Editar" msgstr "Editar"
#: ../views/votes_edit.erb:20 ../views/votes_open.erb:36 #: ../views/votes_edit.erb:20 ../views/votes_open.erb:36
#: ../views/votes_show_closed.erb:22 ../views/votes_show_draft.erb:16 #: ../views/votes_show_draft.erb:16 ../views/votes_show_open.erb:24
#: ../views/votes_show_open.erb:19
msgid "Candidates" msgid "Candidates"
msgstr "Opcions" msgstr "Opcions"
@ -301,8 +320,8 @@ msgid "Open vote to participants"
msgstr "Obrir la votació als votants" msgstr "Obrir la votació als votants"
#: ../views/votes_edit.erb:98 ../views/votes_edit.erb:105 #: ../views/votes_edit.erb:98 ../views/votes_edit.erb:105
#: ../views/votes_show_closed.erb:133 ../views/votes_show_closed.erb:140 #: ../views/votes_show_closed.erb:188 ../views/votes_show_closed.erb:195
#: ../views/votes_show_open.erb:91 ../views/votes_show_open.erb:98 #: ../views/votes_show_open.erb:100 ../views/votes_show_open.erb:107
msgid "Add organizer" msgid "Add organizer"
msgstr "Afegeix organitzadora" msgstr "Afegeix organitzadora"
@ -344,62 +363,108 @@ msgstr "Cancel·lar"
msgid "Closed on %{date}" msgid "Closed on %{date}"
msgstr "Tancada el %{date}" msgstr "Tancada el %{date}"
#: ../views/votes_show_closed.erb:35 #: ../views/votes_show_closed.erb:24
msgid "Results" msgid "Results"
msgstr "Resultats" msgstr "Resultats"
#: ../views/votes_show_closed.erb:39 #: ../views/votes_show_closed.erb:28
msgid "No results are available because nobody voted on this vote." msgid "No results are available because nobody voted on this vote."
msgstr "No hi ha cap resultat perquè ningú va votar en aquesta votació." msgstr "No hi ha cap resultat perquè ningú va votar en aquesta votació."
#: ../views/votes_show_closed.erb:46 #: ../views/votes_show_closed.erb:41
msgid "Rank" msgid "Winning candidate"
msgstr "Rang" msgstr "Opció guanyadora"
#: ../views/votes_show_closed.erb:47 #: ../views/votes_show_closed.erb:43
msgid "Candidate" msgid ""
msgstr "Opció" "The winning candidate is the candidate with the highest median vote (↓), calle"
"d <a href='https://en.wikipedia.org/wiki/Majority_judgment'><i>majority judgme"
"nt</i></a>."
msgstr ""
"L'opció guanyadora és l'opció amb la mediana de vots més alta (↓), anomenada <"
"a href='https://en.wikipedia.org/wiki/Majority_judgment'><i>judici majoritari<"
"/i></a>."
#: ../views/votes_show_closed.erb:48 #: ../views/votes_show_closed.erb:67
msgid "Majority Judgment" msgid "See description"
msgstr "Judici majoritari" msgstr "Veure descripció"
#: ../views/votes_show_closed.erb:49 #: ../views/votes_show_closed.erb:71
msgid "Proponents" msgid "Analyze tiebreak"
msgstr "Defensores" msgstr "Analitzar desempat"
#: ../views/votes_show_closed.erb:50 #: ../views/votes_show_closed.erb:81
msgid "Opponents" msgid "This candidate and the winning candidate have the same majority judgment."
msgstr "Detractores" msgstr "Aquesta opció i l'opció guanyadora tenen el mateix judici majoritari."
#: ../views/votes_show_closed.erb:94 #: ../views/votes_show_closed.erb:83
msgid ""
"To break the tie, we remove the vote that corresponds to the majority judgment"
" one-by-one until one candidate has a better majority judgment than the other."
msgstr ""
"Pel desempat, treiem el vot que correspon al judici majoritari un per un fins "
"que alguna opció tingui un millor judici majoritari que l'altre."
#: ../views/votes_show_closed.erb:85
msgid "Votes for this candidate"
msgstr "Vots per aquesta opció"
#: ../views/votes_show_closed.erb:86
msgid "Votes for the winning candidate"
msgstr "Vots per l'opció guanyadora"
#: ../views/votes_show_closed.erb:93
msgid "Without removing any vote."
msgstr "Sense treure cap vot."
#: ../views/votes_show_closed.erb:98
msgid "Removing 1 vote."
msgstr "Treient 1 vot."
#: ../views/votes_show_closed.erb:103
msgid "Removing %{n} votes."
msgstr "Treient %{n} vots."
#: ../views/votes_show_closed.erb:149
msgid "Participant" msgid "Participant"
msgstr "Votant" msgstr "Votant"
#: ../views/votes_show_closed.erb:118 #: ../views/votes_show_closed.erb:173
msgid "Reopen voting period" msgid "Reopen voting period"
msgstr "Reobrir el període de votació" msgstr "Reobrir el període de votació"
#: ../views/votes_show_open.erb:8 #: ../views/votes_show_open.erb:13
msgid "Closes on %{date}" msgid "Closes on %{date}"
msgstr "Tanca el %{date}" msgstr "Tanca el %{date}"
#: ../views/votes_show_open.erb:24 #: ../views/votes_show_open.erb:29
msgid "Missing rating for candidate <i>%{name}</i>." msgid "Missing rating for candidate <i>%{name}</i>."
msgstr "Falta una valoració per l'opció <i>%{name}</i>." msgstr "Falta una valoració per l'opció <i>%{name}</i>."
#: ../views/votes_show_open.erb:54 #: ../views/votes_show_open.erb:35
msgid "Thank you for voting!"
msgstr "Gràcies per votar!"
#: ../views/votes_show_open.erb:63
msgid "Vote" msgid "Vote"
msgstr "Votar" msgstr "Votar"
#: ../views/votes_show_open.erb:57 #: ../views/votes_show_open.erb:66
msgid "Participants" msgid "Participants"
msgstr "Votants" msgstr "Votants"
#: ../views/votes_show_open.erb:68 #: ../views/votes_show_open.erb:77
msgid "Change back to draft vote" msgid "Change back to draft vote"
msgstr "Tornar a l'esborrany de votació" msgstr "Tornar a l'esborrany de votació"
#: ../views/votes_show_open.erb:72 #: ../views/votes_show_open.erb:81
msgid "Close votes and show results" msgid "Close votes and show results"
msgstr "Tancar la votació i veure els resultats" msgstr "Tancar la votació i veure els resultats"
#: ../views/votes_show_unauthenticated.erb:5
msgid "You need to log in to see the details of this vote."
msgstr "T'has de connectar per veure el detall d'aquesta votació."
#: ../views/votes_show_unknown.erb:2
msgid "Vote not found..."
msgstr "Votació no encontrada..."

View file

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-04 17:25-0600\n" "POT-Creation-Date: 2026-01-13 20:00-0600\n"
"PO-Revision-Date: 2025-12-04 17:25-0600\n" "PO-Revision-Date: 2026-01-13 20:00-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
@ -42,16 +42,20 @@ msgstr ""
msgid "Very good" msgid "Very good"
msgstr "" msgstr ""
#: ../vedia.rb:125 #: ../vedia.rb:143
msgid "Incorrect email or password." msgid "Incorrect email or password."
msgstr "" msgstr ""
#: ../vedia.rb:147 #: ../vedia.rb:170
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr ""
#: ../vedia.rb:582
msgid "Results of the vote: %{t}"
msgstr ""
#: ../views/admin.erb:1 ../views/admin_users.erb:1 ../views/admin_votes.erb:1 #: ../views/admin.erb:1 ../views/admin_users.erb:1 ../views/admin_votes.erb:1
#: ../views/layout.erb:35 #: ../views/layout.erb:30
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
@ -59,7 +63,7 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" msgstr ""
#: ../views/admin.erb:13 ../views/layout.erb:31 ../views/votes.erb:1 #: ../views/admin.erb:13 ../views/layout.erb:26 ../views/votes.erb:1
msgid "Votes" msgid "Votes"
msgstr "" msgstr ""
@ -111,7 +115,7 @@ msgid "No vote organized."
msgstr "" msgstr ""
#: ../views/admin_users.erb:30 ../views/admin_votes.erb:38 #: ../views/admin_users.erb:30 ../views/admin_votes.erb:38
#: ../views/votes_show_closed.erb:89 #: ../views/votes_show_closed.erb:144
msgid "Ratings" msgid "Ratings"
msgstr "" msgstr ""
@ -148,8 +152,8 @@ msgid "State: %{state}"
msgstr "" msgstr ""
#: ../views/admin_votes.erb:19 ../views/votes_edit.erb:90 #: ../views/admin_votes.erb:19 ../views/votes_edit.erb:90
#: ../views/votes_show_closed.erb:123 ../views/votes_show_draft.erb:29 #: ../views/votes_show_closed.erb:178 ../views/votes_show_draft.erb:29
#: ../views/votes_show_open.erb:79 #: ../views/votes_show_open.erb:88
msgid "Organizers" msgid "Organizers"
msgstr "" msgstr ""
@ -189,34 +193,41 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: ../views/layout.erb:40 #: ../views/layout.erb:35
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
#: ../views/login.erb:1 ../views/login.erb:16 #: ../views/login.erb:1 ../views/login.erb:21
#: ../views/votes_show_unauthenticated.erb:16
msgid "Login" msgid "Login"
msgstr "" msgstr ""
#: ../views/login.erb:9 ../views/reset.erb:5 ../views/reset_change.erb:13 #: ../views/login.erb:6 ../views/reset.erb:1 ../views/reset.erb:16
#: ../views/signup.erb:24 ../views/votes_edit.erb:102
#: ../views/votes_show_closed.erb:137 ../views/votes_show_open.erb:95
msgid "Email"
msgstr ""
#: ../views/login.erb:13 ../views/reset_change.erb:17 ../views/signup.erb:28
msgid "Password"
msgstr ""
#: ../views/login.erb:18 ../views/signup.erb:1 ../views/signup.erb:31
msgid "Create account"
msgstr ""
#: ../views/login.erb:19 ../views/reset.erb:1 ../views/reset.erb:8
#: ../views/reset_change.erb:1 ../views/reset_change.erb:20 #: ../views/reset_change.erb:1 ../views/reset_change.erb:20
#: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1 #: ../views/reset_invalid.erb:1 ../views/reset_sent.erb:1
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: ../views/login.erb:12 ../views/reset.erb:13 ../views/reset_change.erb:13
#: ../views/signup.erb:24 ../views/votes_edit.erb:102
#: ../views/votes_show_closed.erb:192 ../views/votes_show_open.erb:104
#: ../views/votes_show_unauthenticated.erb:9
msgid "Email"
msgstr ""
#: ../views/login.erb:16 ../views/reset_change.erb:17 ../views/signup.erb:28
#: ../views/votes_show_unauthenticated.erb:13
msgid "Password"
msgstr ""
#: ../views/login.erb:23 ../views/signup.erb:1 ../views/signup.erb:32
msgid "Create account"
msgstr ""
#: ../views/reset.erb:6
msgid "Enter an email address."
msgstr ""
#: ../views/reset_change.erb:6 ../views/signup.erb:17 #: ../views/reset_change.erb:6 ../views/signup.erb:17
msgid "Enter a password." msgid "Enter a password."
msgstr "" msgstr ""
@ -251,12 +262,20 @@ msgstr ""
msgid "An account already exists for %{email}." msgid "An account already exists for %{email}."
msgstr "" msgstr ""
#: ../views/votes.erb:18 ../views/votes_edit.erb:6 #: ../views/votes.erb:18 ../views/votes_show_open.erb:7
msgid "Voted"
msgstr ""
#: ../views/votes.erb:20 ../views/votes_show_open.erb:9
msgid "Not voted"
msgstr ""
#: ../views/votes.erb:23 ../views/votes_edit.erb:6
#: ../views/votes_show_closed.erb:7 #: ../views/votes_show_closed.erb:7
msgid "Organizer" msgid "Organizer"
msgstr "" msgstr ""
#: ../views/votes.erb:28 ../views/votes_new.erb:21 #: ../views/votes.erb:33 ../views/votes_new.erb:21
msgid "Create new vote" msgid "Create new vote"
msgstr "" msgstr ""
@ -273,8 +292,7 @@ msgid "Edit"
msgstr "" msgstr ""
#: ../views/votes_edit.erb:20 ../views/votes_open.erb:36 #: ../views/votes_edit.erb:20 ../views/votes_open.erb:36
#: ../views/votes_show_closed.erb:22 ../views/votes_show_draft.erb:16 #: ../views/votes_show_draft.erb:16 ../views/votes_show_open.erb:24
#: ../views/votes_show_open.erb:19
msgid "Candidates" msgid "Candidates"
msgstr "" msgstr ""
@ -299,8 +317,8 @@ msgid "Open vote to participants"
msgstr "" msgstr ""
#: ../views/votes_edit.erb:98 ../views/votes_edit.erb:105 #: ../views/votes_edit.erb:98 ../views/votes_edit.erb:105
#: ../views/votes_show_closed.erb:133 ../views/votes_show_closed.erb:140 #: ../views/votes_show_closed.erb:188 ../views/votes_show_closed.erb:195
#: ../views/votes_show_open.erb:91 ../views/votes_show_open.erb:98 #: ../views/votes_show_open.erb:100 ../views/votes_show_open.erb:107
msgid "Add organizer" msgid "Add organizer"
msgstr "" msgstr ""
@ -340,62 +358,103 @@ msgstr ""
msgid "Closed on %{date}" msgid "Closed on %{date}"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:35 #: ../views/votes_show_closed.erb:24
msgid "Results" msgid "Results"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:39 #: ../views/votes_show_closed.erb:28
msgid "No results are available because nobody voted on this vote." msgid "No results are available because nobody voted on this vote."
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:46 #: ../views/votes_show_closed.erb:41
msgid "Rank" msgid "Winning candidate"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:47 #: ../views/votes_show_closed.erb:43
msgid "Candidate" msgid ""
"The winning candidate is the candidate with the highest median vote (↓), calle"
"d <a href='https://en.wikipedia.org/wiki/Majority_judgment'><i>majority judgme"
"nt</i></a>."
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:48 #: ../views/votes_show_closed.erb:67
msgid "Majority Judgment" msgid "See description"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:49 #: ../views/votes_show_closed.erb:71
msgid "Proponents" msgid "Analyze tiebreak"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:50 #: ../views/votes_show_closed.erb:81
msgid "Opponents" msgid "This candidate and the winning candidate have the same majority judgment."
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:94 #: ../views/votes_show_closed.erb:83
msgid ""
"To break the tie, we remove the vote that corresponds to the majority judgment"
" one-by-one until one candidate has a better majority judgment than the other."
msgstr ""
#: ../views/votes_show_closed.erb:85
msgid "Votes for this candidate"
msgstr ""
#: ../views/votes_show_closed.erb:86
msgid "Votes for the winning candidate"
msgstr ""
#: ../views/votes_show_closed.erb:93
msgid "Without removing any vote."
msgstr ""
#: ../views/votes_show_closed.erb:98
msgid "Removing 1 vote."
msgstr ""
#: ../views/votes_show_closed.erb:103
msgid "Removing %{n} votes."
msgstr ""
#: ../views/votes_show_closed.erb:149
msgid "Participant" msgid "Participant"
msgstr "" msgstr ""
#: ../views/votes_show_closed.erb:118 #: ../views/votes_show_closed.erb:173
msgid "Reopen voting period" msgid "Reopen voting period"
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:8 #: ../views/votes_show_open.erb:13
msgid "Closes on %{date}" msgid "Closes on %{date}"
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:24 #: ../views/votes_show_open.erb:29
msgid "Missing rating for candidate <i>%{name}</i>." msgid "Missing rating for candidate <i>%{name}</i>."
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:54 #: ../views/votes_show_open.erb:35
msgid "Thank you for voting!"
msgstr ""
#: ../views/votes_show_open.erb:63
msgid "Vote" msgid "Vote"
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:57 #: ../views/votes_show_open.erb:66
msgid "Participants" msgid "Participants"
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:68 #: ../views/votes_show_open.erb:77
msgid "Change back to draft vote" msgid "Change back to draft vote"
msgstr "" msgstr ""
#: ../views/votes_show_open.erb:72 #: ../views/votes_show_open.erb:81
msgid "Close votes and show results" msgid "Close votes and show results"
msgstr "" msgstr ""
#: ../views/votes_show_unauthenticated.erb:5
msgid "You need to log in to see the details of this vote."
msgstr ""
#: ../views/votes_show_unknown.erb:2
msgid "Vote not found..."
msgstr ""

File diff suppressed because it is too large Load diff

4494
public/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,6 @@ require 'sinatra/activerecord'
require 'bcrypt' require 'bcrypt'
require 'gettext' require 'gettext'
require 'securerandom' require 'securerandom'
require 'chartkick'
require 'mail' require 'mail'
require 'ostruct' require 'ostruct'
require 'tzinfo' require 'tzinfo'
@ -580,7 +579,7 @@ def close_expired_votes
mail = Mail.new mail = Mail.new
mail.from = settings.admin_email mail.from = settings.admin_email
mail.to = user.email mail.to = user.email
mail.subject = _("Results of the vote: #{vote.title}") mail.subject = _("Results of the vote: %{t}") % { t: vote.title }
template = ERB.new(File.read("views/votes_close_email.erb")) template = ERB.new(File.read("views/votes_close_email.erb"))
mail.body = template.result(binding) mail.body = template.result(binding)
mail.deliver mail.deliver
@ -593,6 +592,8 @@ helpers do
if session[:user_id] if session[:user_id]
User.find(session[:user_id]) User.find(session[:user_id])
elsif settings.spoof_admin elsif settings.spoof_admin
session.clear
session[:timezone] = 'UTC'
User.find(1) User.find(1)
else else
nil nil

View file

@ -9,8 +9,7 @@
<% else %> <% else %>
<title>Vedia</title> <title>Vedia</title>
<% end %> <% end %>
<script src="/chartkick.js"></script> <script src="/bootstrap.js"></script>
<script src="/Chart.bundle.js"></script>
<link rel="stylesheet" href="/bootstrap.css"> <link rel="stylesheet" href="/bootstrap.css">
<link rel="stylesheet" href="/bootstrap-icons.css"> <link rel="stylesheet" href="/bootstrap-icons.css">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

View file

@ -17,4 +17,8 @@
background-color: <%= v[:color] %>; background-color: <%= v[:color] %>;
} }
.mj-cell-<%= v[:id] %> {
background-color: <%= v[:color] %> !important;
}
<% end %> <% end %>

View file

@ -19,17 +19,6 @@
</div> </div>
</div> </div>
<h2 class="mb-4"><%= _("Candidates") %></h2>
<% @vote.candidates.each do |candidate| %>
<div class="card mb-4">
<div class="card-body">
<h3 class="mb-3"><%= candidate.name %></h3>
<%= markdown(candidate.description) %>
</div>
</div>
<% end %>
<div class="mb-5"></div> <div class="mb-5"></div>
<h2 class="mb-4"><%= _("Results") %></h2> <h2 class="mb-4"><%= _("Results") %></h2>
@ -40,55 +29,121 @@
<% else %> <% else %>
<table class="table mb-4"> <% r = 0 %>
<tr> <% @results = @vote.candidates.sort { |a, b| a.mj <=> b.mj }.reverse %>
<th></th> <% @results.each do |c| %>
<th><%= _("Rank") %></th> <% r = r + 1 %>
<th><%= _("Candidate") %></th> <div class="card mb-4">
<th><%= _("Majority Judgment") %></th> <div class="card-body">
<th><%= _("Proponents") %></th> <% if r == 1 %>
<th><%= _("Opponents") %></th> <p class="fs-5">
</tr> <i class="bi bi-trophy-fill"></i>
<% i = 0 %> <%= _("Winning candidate") %>
<% @vote.candidates.sort { |a, b| a.mj <=> b.mj }.reverse.each do |candidate| %> </p>
<% i = i + 1 %> <p><%= _("The winning candidate is the candidate with the highest median vote (↓), called <a href='https://en.wikipedia.org/wiki/Majority_judgment'><i>majority judgment</i></a>.") %></p>
<% if i == 1 %>
<tr class="table-success">
<td><i class="bi bi-trophy-fill"></i></td>
<td><%= i %></i></td>
<% else %>
<tr>
<td></td>
<td><%= i %></td>
<% end %> <% end %>
<td><%= candidate.name %></td> <p class="fs-5">
<% value = settings.values.select { |e| e[:id] == candidate.mj.mj }.first %> <%= "##{r}" %>
<td class="h5"><span class="badge bg-<%= value[:id] %>"><%= _(value[:label]) %></span></td> <% value = settings.values.select { |e| e[:id] == c.mj.mj }.first %>
<td><%= candidate.mj.proponents %></td> <span class="badge bg-<%= value[:id] %>"><%= _(value[:label]) %></span>
<td><%= candidate.mj.opponents %></td> </p>
</tr> <h3><%= c.name %></h3>
<table class="table table-borderless">
<tr>
<% (1..c.mj.n).each do |i| %>
<td class="text-center <% unless i == c.mj.majority %>opacity-0<% end %>">↓</td>
<% end %> <% end %>
</table> </tr>
<tr>
<% <% settings.values.reverse.each do |v| %>
data = [] <% (1..c.mj.count[v[:id]]).each do |r| %>
colors = [] <td class="mj-cell-<%= v[:id] %>">&nbsp;</td>
settings.values.reverse.each do |v| <% end %>
d = { name: _(v[:label]), data: [] } <% end %>
@vote.candidates.sort { |a, b| a.mj <=> b.mj }.reverse.each do |c| </tr>
d[:data] << [c.name, c.mj.count[v[:id]]] </table>
end <p class="d-inline-flex gap-3">
data << d <a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#description-<%= r %>" role="button" aria-expanded="false" aria-controls="description-<%= r %>">
colors << v[:color] <%= _("See description") %>
end </a>
%> <% if r > 1 and c.mj.mj == @results.first.mj.mj %>
<%= bar_chart data, colors: colors, stacked: true, legend: 'bottom' %> <a class="btn btn-primary" data-bs-toggle="collapse" href="#tiebreak-<%= r %>" role="button" aria-expanded="false" aria-controls="tiebreak-<%= r %>">
<%= _("Analyze tiebreak") %>
</a>
<% end %>
</p>
<div class="collapse" id="description-<%= r %>">
<%= markdown(c.description) %>
</div>
<% if r > 1 and c.mj.mj == @results.first.mj.mj %>
<div class="collapse" id="tiebreak-<%= r %>">
<p>
<%= _("This candidate and the winning candidate have the same majority judgment.") %>
</p>
<p><%= _("To break the tie, we remove the vote that corresponds to the majority judgment one-by-one until one candidate has a better majority judgment than the other.") %></p>
<div class="row fw-bold">
<div class="col"><%= _("Votes for this candidate") %></div>
<div class="col text-end"><%= _("Votes for the winning candidate") %></div>
</div>
<% n = 0 %>
<% c.mj.break_tie(@results.first.mj).each do |a, b| %>
<hr>
<% if n == 0 %>
<div class="row">
<div class="col"><%= _("Without removing any vote.") %></div>
</div>
<% end %>
<% if n == 1 %>
<div class="row">
<div class="col"><%= _("Removing 1 vote.") %></div>
</div>
<% end %>
<% if n > 1 %>
<div class="row">
<div class="col"><%= _("Removing %{n} votes.") % { n: n } %></div>
</div>
<% end %>
<table class="table table-borderless">
<tr>
<% (1..a.n).each do |i| %>
<td class="text-center <% unless i == a.majority %>opacity-0<% end %>">↓</td>
<% end %>
<th class="w-100"></th>
<% (1..b.n).each do |i| %>
<td class="text-center <% unless b.n - i + 1 == b.majority %>opacity-0<% end %>">↓</td>
<% end %>
</tr>
<tr>
<% settings.values.reverse.each do |v| %>
<% (1..a.count[v[:id]]).each do |r| %>
<td class="mj-cell-<%= v[:id] %>">&nbsp;</td>
<% end %>
<% end %>
<td class="text-center fw-bold">
<% if a.mj < b.mj %><<% end %>
<% if a.mj > b.mj %>><% end %>
<% if a.mj == b.mj %>=<% end %>
</td>
<% settings.values.each do |v| %>
<% (1..b.count[v[:id]]).each do |r| %>
<td class="mj-cell-<%= v[:id] %>">&nbsp;</td>
<% end %>
<% end %>
</tr>
</table>
<% n = n + 1 %>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="mb-5"></div> <div class="mb-5"></div>
<h2 class="mb-4"><%= _("Ratings") %></h2> <h2 class="mb-4"><%= _("Ratings") + " (#{@vote.ratings.collect { |rating| rating.user }.uniq.count})" %></h2>
<table class="table table-striped mb-5"> <table class="table table-striped w-auto mb-5">
<thead> <thead>
<tr> <tr>
<th><%= _("Participant") %></th> <th><%= _("Participant") %></th>

View file

@ -32,7 +32,7 @@
<% end %> <% end %>
<% if @voted %> <% if @voted %>
<p class="alert alert-success mb-4"><%= _("Gràcies per votar!") %></p> <p class="alert alert-success mb-4"><%= _("Thank you for voting!") %></p>
<% end %> <% end %>
<form action="/votes/<%= @vote.secure_id %>/ratings" method="post" class="mb-5"> <form action="/votes/<%= @vote.secure_id %>/ratings" method="post" class="mb-5">