decidim-elections 0.31.0 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/app/commands/decidim/elections/admin/update_election.rb +3 -3
  3. data/app/controllers/concerns/decidim/elections/uses_votes_booth.rb +8 -2
  4. data/app/controllers/decidim/elections/per_question_votes_controller.rb +28 -1
  5. data/app/forms/decidim/elections/admin/election_form.rb +10 -0
  6. data/app/forms/decidim/elections/censuses/internal_users_form.rb +7 -13
  7. data/app/models/decidim/elections/election.rb +4 -8
  8. data/app/packs/entrypoints/decidim_elections.js +1 -0
  9. data/app/packs/src/decidim/elections/live_results_update.js +22 -0
  10. data/app/packs/src/decidim/elections/question_status_checker.js +42 -0
  11. data/app/permissions/decidim/elections/admin/permissions.rb +3 -5
  12. data/app/presenters/decidim/elections/election_presenter.rb +8 -3
  13. data/app/views/decidim/elections/admin/dashboard/_questions_with_results.html.erb +5 -0
  14. data/app/views/decidim/elections/admin/elections/_election-tr.html.erb +1 -1
  15. data/app/views/decidim/elections/admin/elections/_form.html.erb +1 -1
  16. data/app/views/decidim/elections/admin/questions/_form.html.erb +1 -30
  17. data/app/views/decidim/elections/censuses/_internal_users_form.html.erb +15 -0
  18. data/app/views/decidim/elections/elections/_election_aside.html.erb +2 -1
  19. data/app/views/decidim/elections/elections/_vote_results_question.html.erb +4 -0
  20. data/app/views/decidim/elections/per_question_votes/show.html.erb +2 -0
  21. data/app/views/decidim/elections/votes/receipt.html.erb +18 -1
  22. data/config/locales/ca-IT.yml +7 -3
  23. data/config/locales/ca.yml +7 -3
  24. data/config/locales/cs.yml +6 -1
  25. data/config/locales/de.yml +4 -0
  26. data/config/locales/en.yml +8 -4
  27. data/config/locales/es-MX.yml +7 -3
  28. data/config/locales/es-PY.yml +7 -3
  29. data/config/locales/es.yml +7 -3
  30. data/config/locales/eu.yml +8 -4
  31. data/config/locales/fi-plain.yml +7 -3
  32. data/config/locales/fi.yml +7 -3
  33. data/config/locales/fr-CA.yml +8 -4
  34. data/config/locales/fr.yml +8 -4
  35. data/config/locales/ja.yml +6 -3
  36. data/config/locales/pt-BR.yml +194 -0
  37. data/config/locales/sv.yml +10 -3
  38. data/lib/decidim/elections/admin_engine.rb +4 -4
  39. data/lib/decidim/elections/test/per_question_vote_examples.rb +105 -3
  40. data/lib/decidim/elections/test/vote_examples.rb +5 -3
  41. data/lib/decidim/elections/version.rb +1 -1
  42. metadata +15 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ec49644173bf7e80079c7743d7eb81ba6ba300a744c1001b1175315bd150d8e
4
- data.tar.gz: 2ed772499e951961cc6cd12efad4df11ad9afb0b54879458674bbb4bcd2221f2
3
+ metadata.gz: 3314659cca0234480f9fbca9edd5ebd51d55f465d8e66608aadf327c04101d73
4
+ data.tar.gz: 3bbb9c90945c6c7e14e37e993d1095ce69e78ef7508f9790186a38449b5f36d5
5
5
  SHA512:
6
- metadata.gz: 60d40111642b60f26d17c7b32fe0d6f5a03853634c3e24224762e24682ec02234cff2e1f7e2654577a9f8f743df6a1feeb795556a27d49389a50111f442ddf33
7
- data.tar.gz: 6a02d692108948fab7732182a94a36c0b49b01cd069be61ed744d05717e43073000107280020724c4b684e3cc067a6bd10871ce646cc874b820a1b52ce9da937
6
+ metadata.gz: e0012585bd4d6b44592a964be98666b30c20ff3abf68ebc892ba922ab8465dfba059afa347000b08ad8e2057bc66f3aa3caaaa457045ce70df85ab07ba5b9992
7
+ data.tar.gz: f17982cf8116fbfdce4c5a0258f07f5beac98a436e627fac163035da027812cd44e64e0ecc42d83dd02423e25a5df9035d0c1229f6c7a2f02e417111f4c2e1b5
@@ -17,14 +17,14 @@ module Decidim
17
17
  alias election resource
18
18
 
19
19
  def attributes
20
- election.published? ? published_election_attributes : unpublished_election_attributes
20
+ election.started? ? started_election_attributes : not_started_election_attributes
21
21
  end
22
22
 
23
- def published_election_attributes
23
+ def started_election_attributes
24
24
  { description: parsed_description }
25
25
  end
26
26
 
27
- def unpublished_election_attributes
27
+ def not_started_election_attributes
28
28
  {
29
29
  title: parsed_title,
30
30
  description: parsed_description,
@@ -47,10 +47,16 @@ module Decidim
47
47
 
48
48
  # Shows the receipt page
49
49
  def receipt
50
+ if params[:exit].present?
51
+ votes_buffer.clear
52
+ session_attributes.clear
53
+ return redirect_to(exit_path)
54
+ end
55
+
50
56
  enforce_permission_to(:create, :vote, election:)
51
57
 
52
- votes_buffer.clear
53
- session_attributes.clear
58
+ votes_buffer.clear unless election.per_question?
59
+
54
60
  return redirect_to(exit_path) unless election.votes.exists?(voter_uid: session[:voter_uid])
55
61
 
56
62
  render "decidim/elections/votes/receipt"
@@ -16,12 +16,20 @@ module Decidim
16
16
  end
17
17
 
18
18
  before_action only: [:show, :update] do
19
- redirect_to(**next_vote_step_action) unless question.voting_enabled?
19
+ redirect_to(**next_vote_step_action) unless question.voting_enabled? || request.format.json?
20
20
  end
21
21
 
22
22
  # Show the voting form for the given question
23
+ # Responds to HTML (render the form) and JSON (check question status for polling)
23
24
  def show
24
25
  enforce_permission_to(:create, :vote, election:)
26
+
27
+ respond_to do |format|
28
+ format.html
29
+ format.json do
30
+ render json: question_status_response
31
+ end
32
+ end
25
33
  end
26
34
 
27
35
  # Saves the vote for the current question and redirect to the next question
@@ -29,6 +37,7 @@ module Decidim
29
37
  enforce_permission_to(:create, :vote, election:)
30
38
 
31
39
  response_ids = params.dig(:response, question.id.to_s)
40
+ requeue_following_questions
32
41
  votes_buffer[question.id.to_s] = response_ids
33
42
  CastVotes.call(election, { question.id.to_s => response_ids }, voter_uid) do
34
43
  on(:ok) do
@@ -84,6 +93,24 @@ module Decidim
84
93
 
85
94
  { action: :show, id: next_question }
86
95
  end
96
+
97
+ # Returns JSON response with question voting status for client-side polling.
98
+ # Used to redirect users when a question's voting is closed while they are on the voting page.
99
+ def question_status_response
100
+ if question.voting_enabled?
101
+ { voting_enabled: true, redirect_url: nil }
102
+ else
103
+ redirect_action = next_vote_step_action
104
+ { voting_enabled: false, redirect_url: url_for(**redirect_action) }
105
+ end
106
+ end
107
+
108
+ def requeue_following_questions
109
+ election.questions
110
+ .where("position > ?", question.position)
111
+ .pluck(:id)
112
+ .each { |id| votes_buffer.delete(id.to_s) }
113
+ end
87
114
  end
88
115
  end
89
116
  end
@@ -25,9 +25,11 @@ module Decidim
25
25
  validates :title, translatable_presence: true
26
26
  validates :results_availability, inclusion: { in: Decidim::Elections::Election::RESULTS_AVAILABILITY_OPTIONS }
27
27
  validates :start_at, date: { before: :end_at }, unless: :manual_start?
28
+ validates :start_at, date: { after: proc { Time.current } }, if: :scheduled_election?
28
29
  validates :manual_start, acceptance: true, if: :per_question_not_started?
29
30
  validates :end_at, presence: true
30
31
  validates :end_at, date: { after: :start_at }, if: ->(f) { f.start_at.present? && f.end_at.present? }
32
+ validates :end_at, date: { after: proc { Time.current } }, if: :scheduled_election?
31
33
 
32
34
  def map_model(election)
33
35
  self.manual_start = election.start_at.blank?
@@ -42,6 +44,14 @@ module Decidim
42
44
  def per_question_not_started?
43
45
  results_availability == "per_question" && start_at.blank?
44
46
  end
47
+
48
+ def election
49
+ @election ||= context[:election]
50
+ end
51
+
52
+ def scheduled_election?
53
+ election&.scheduled?
54
+ end
45
55
  end
46
56
  end
47
57
  end
@@ -8,6 +8,9 @@ module Decidim
8
8
  validate :user_authenticated
9
9
 
10
10
  delegate :election, :current_user, to: :context
11
+ delegate :organization, to: :current_user
12
+
13
+ attr_reader :authorization_status
11
14
 
12
15
  def voter_uid
13
16
  @voter_uid ||= election.census.users(election).find_by(id: current_user&.id)&.to_global_id&.to_s
@@ -18,7 +21,7 @@ module Decidim
18
21
  end
19
22
 
20
23
  def authorization_handlers
21
- election.census_settings["authorization_handlers"] || {}
24
+ @authorization_handlers ||= election.census_settings&.fetch("authorization_handlers", {})&.slice(*organization.available_authorizations)
22
25
  end
23
26
 
24
27
  def authorizations
@@ -26,7 +29,7 @@ module Decidim
26
29
  [
27
30
  adapter,
28
31
  Decidim::Verifications::Authorizations.new(
29
- organization: current_user.organization,
32
+ organization: organization,
30
33
  user: current_user,
31
34
  name: adapter.name
32
35
  ).first
@@ -43,20 +46,11 @@ module Decidim
43
46
  def user_authenticated
44
47
  return errors.add(:base, I18n.t("decidim.elections.censuses.internal_users_form.invalid")) unless in_census?
45
48
 
46
- invalid = authorizations.filter_map do |adapter, authorization|
47
- if !authorization || !authorization.granted?
48
- ["not_granted", adapter]
49
- elsif adapter.authorize(authorization, authorization_handlers.dig(adapter.name, "options"), election.component, election)&.first != :ok
50
- ["not_authorized", adapter]
51
- end
52
- end
49
+ @authorization_status = Decidim::ActionAuthorizer::AuthorizationStatusCollection.new(authorization_handlers, current_user, election.component, election)
53
50
 
54
- return if invalid.empty?
51
+ return if @authorization_status.ok?
55
52
 
56
53
  errors.add(:base, I18n.t("decidim.elections.censuses.internal_users_form.invalid"))
57
- invalid.each do |error, adapter|
58
- errors.add(:base, I18n.t("decidim.elections.censuses.internal_users_form.#{error}", adapter: adapter.fullname))
59
- end
60
54
  end
61
55
  end
62
56
  end
@@ -72,6 +72,10 @@ module Decidim
72
72
  published? && !ongoing? && !finished? && !published_results?
73
73
  end
74
74
 
75
+ def editable?
76
+ published? ? !started? : !votes.exists?
77
+ end
78
+
75
79
  def started?
76
80
  start_at.present? && start_at <= Time.current
77
81
  end
@@ -131,14 +135,6 @@ module Decidim
131
135
  questions
132
136
  end
133
137
 
134
- # Create i18n ransackers for :title and :description.
135
- # Create the :search_text ransacker alias for searching from both of these.
136
- ransacker_i18n_multi :search_text, [:title, :description]
137
-
138
- def self.ransackable_scopes(_auth_object = nil)
139
- [:with_any_state]
140
- end
141
-
142
138
  def status
143
139
  return @status if defined?(@status)
144
140
 
@@ -1,5 +1,6 @@
1
1
  import "src/decidim/elections/waiting_room.js"
2
2
  import "src/decidim/elections/live_results_update.js";
3
+ import "src/decidim/elections/question_status_checker.js";
3
4
 
4
5
  // Images
5
6
  require.context("../images", true)
@@ -12,6 +12,7 @@ document.addEventListener("DOMContentLoaded", () => {
12
12
  const optionVoteCountTexts = () => document.querySelectorAll("[data-option-votes-count-text]");
13
13
  const optionVotePercentTexts = () => document.querySelectorAll("[data-option-votes-percent-text]");
14
14
  const optionVoteWidths = () => document.querySelectorAll("[data-option-votes-width]");
15
+ const questionTotalVotesTexts = () => document.querySelectorAll("[data-question-total-votes-text]");
15
16
 
16
17
  const animateText = (element, value) => {
17
18
  if (element.textContent === value) {
@@ -24,6 +25,15 @@ document.addEventListener("DOMContentLoaded", () => {
24
25
  }, 1000);
25
26
  };
26
27
 
28
+ const digQuestionValue = (questionId, data, key) => {
29
+ const questions = data.questions || [];
30
+ const question = questions.find((item) => item.id === parseInt(questionId, 10));
31
+ if (!question || !(key in question)) {
32
+ return null;
33
+ }
34
+ return question[key];
35
+ };
36
+
27
37
  const digOptionValue = (questionId, optionId, data, key) => {
28
38
  data.questions = data.questions || [];
29
39
  const question = data.questions.find((item) => item.id === parseInt(questionId, 10));
@@ -55,6 +65,11 @@ document.addEventListener("DOMContentLoaded", () => {
55
65
  questionElement.id = `question-${question.id}`;
56
66
  questionElement.classList.remove("hidden");
57
67
  questionElement.querySelector("[data-question-body]").textContent = question.body;
68
+ const totalVotesElement = questionElement.querySelector("[data-question-total-votes-text]");
69
+ if (totalVotesElement) {
70
+ totalVotesElement.dataset.questionTotalVotesText = question.id;
71
+ totalVotesElement.textContent = question.total_votes_text || "";
72
+ }
58
73
  const optionsContainer = questionElement.querySelector("[data-options-container]");
59
74
  question.response_options.forEach((option) => {
60
75
  const optionElement = optionTemplate.cloneNode(true);
@@ -106,6 +121,13 @@ document.addEventListener("DOMContentLoaded", () => {
106
121
  el.style.width = `${val}%`;
107
122
  }
108
123
  });
124
+ questionTotalVotesTexts().forEach((el) => {
125
+ const questionId = el.dataset.questionTotalVotesText;
126
+ const val = digQuestionValue(questionId, data, "total_votes_text");
127
+ if (val) {
128
+ animateText(el, val);
129
+ }
130
+ });
109
131
  // repeat for ongoing elections only
110
132
  if (data.ongoing) {
111
133
  setTimeout(fetchResults, 4000);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Polls the server to check if the current question is still available for voting.
3
+ * When voting is closed, redirects user to the waiting room or next question.
4
+ */
5
+ document.addEventListener("DOMContentLoaded", () => {
6
+ const votingBooth = document.querySelector("[data-question-status-url]");
7
+ if (!votingBooth) {
8
+ return;
9
+ }
10
+
11
+ const statusUrl = votingBooth.dataset.questionStatusUrl;
12
+ if (!statusUrl) {
13
+ return;
14
+ }
15
+
16
+ const checkStatus = async () => {
17
+ try {
18
+ const response = await fetch(statusUrl, {
19
+ method: "GET",
20
+ headers: {
21
+ "Accept": "application/json",
22
+ "Content-Type": "application/json",
23
+ "X-Requested-With": "XMLHttpRequest"
24
+ }
25
+ });
26
+
27
+ if (response.ok) {
28
+ const result = await response.json();
29
+ if (!result.voting_enabled && result.redirect_url) {
30
+ window.location.href = result.redirect_url;
31
+ return;
32
+ }
33
+ }
34
+ } catch (error) {
35
+ console.error("[QuestionStatusChecker] Error:", error);
36
+ }
37
+
38
+ setTimeout(checkStatus, 1000);
39
+ };
40
+
41
+ setTimeout(checkStatus, 1000);
42
+ });
@@ -43,7 +43,7 @@ module Decidim
43
43
 
44
44
  case permission_action.action
45
45
  when :update, :reorder
46
- toggle_allow(election.present? && !election.published?)
46
+ toggle_allow(election.present? && election.editable?)
47
47
  when :update_status
48
48
  toggle_allow(election.present? && election.published? && election.questions.exists?)
49
49
  end
@@ -53,10 +53,8 @@ module Decidim
53
53
  return unless permission_action.subject == :census
54
54
 
55
55
  case permission_action.action
56
- when :edit
57
- allow!
58
- when :update
59
- toggle_allow(election.present? && !election.published?)
56
+ when :edit, :update
57
+ toggle_allow(election.present? && election.editable?)
60
58
  end
61
59
  end
62
60
  end
@@ -5,7 +5,6 @@ module Decidim
5
5
  class ElectionPresenter < Decidim::ResourcePresenter
6
6
  include Decidim::ResourceHelper
7
7
  include ActionView::Helpers::UrlHelper
8
- include Decidim::SanitizeHelper
9
8
 
10
9
  def election
11
10
  __getobj__
@@ -41,7 +40,13 @@ module Decidim
41
40
  body: translated_attribute(question.body),
42
41
  position: question.position,
43
42
  voting_enabled: question.voting_enabled?,
44
- published_results: question.published_results?,
43
+ published_results: question.published_results?
44
+ }.tap do |hash|
45
+ next unless admin || result_published_questions.include?(question)
46
+
47
+ hash[:total_votes] = question.total_votes
48
+ hash[:total_votes_text] = I18n.t("total_votes", scope: "decidim.elections.elections.vote_results", count: question.total_votes)
49
+ end.merge(
45
50
  response_options: question.response_options.map do |option|
46
51
  {
47
52
  id: option.id,
@@ -55,7 +60,7 @@ module Decidim
55
60
  hash[:votes_percent] = option.votes_percent
56
61
  end
57
62
  end
58
- }
63
+ )
59
64
  end
60
65
  }
61
66
  end
@@ -35,6 +35,11 @@
35
35
  <td data-option-votes-percent-text="<%= question.id %>,<%= option.id %>"><%= number_to_percentage(option.votes_percent, precision: 1) %></td>
36
36
  </tr>
37
37
  <% end %>
38
+ <tr>
39
+ <td class="w-2/3 border-none"><%= t("decidim.elections.admin.dashboard.questions_table.total") %></td>
40
+ <td class="border-none"></td>
41
+ <td class="border-none" data-question-total-votes-text="<%= question.id %>"><%= t("votes_count", scope: "decidim.elections.admin.dashboard.questions_table", count: question.total_votes) %></td>
42
+ </tr>
38
43
  </tbody>
39
44
  </table>
40
45
  </div>
@@ -1,7 +1,7 @@
1
1
  <tr data-id="<%= election.id %>">
2
2
  <td data-label="<%= t("models.election.fields.title", scope: "decidim.elections") %>">
3
3
  <% if allowed_to? :update, :election, election: election %>
4
- <%= link_to present(election).title(html_escape: true), election.published? ? dashboard_election_path(election) : edit_election_path(election) %>
4
+ <%= link_to present(election).title(html_escape: true), election.started? ? dashboard_election_path(election) : edit_election_path(election) %>
5
5
  <% else %>
6
6
  <%= present(election).title(html_escape: true) %>
7
7
  <% end %>
@@ -1,4 +1,4 @@
1
- <% disable_fields = @election&.published? %>
1
+ <% disable_fields = @election.present? && !@election.editable? %>
2
2
  <div class="form__wrapper">
3
3
  <div class="card-section election-fields">
4
4
  <div class="card form-basic" data-controller="accordion" id="accordion-basic">
@@ -16,7 +16,7 @@
16
16
  <%= render "decidim/elections/admin/questions/response_option_template", form: question_form, editable: true, template_id: "response-option-template-dummy" %>
17
17
  <% end %>
18
18
 
19
- <div class="questionnaire-questions-list flex flex-col py-6 gap-6 last:pb-0" data-draggable-table data-sort-url="#" id="questionnaire-questions-list">
19
+ <div class="questionnaire-questions-list flex flex-col py-6 gap-6 last:pb-0" data-draggable-table data-sort-url="#" data-draggable-handle=".card-divider" id="questionnaire-questions-list">
20
20
  <% @form.questions.each_with_index do |question, index| %>
21
21
  <%= fields_for "questions[]", question do |question_form| %>
22
22
  <%= render "decidim/elections/admin/questions/question",
@@ -35,34 +35,5 @@
35
35
  <script>
36
36
  document.addEventListener("turbo:load", function () {
37
37
  window.Decidim.createEditableForm();
38
-
39
- // Function to initialize the sortable functionality
40
- function initializeSortable() {
41
- const container = document.querySelector("#questionnaire-questions-list");
42
- const questionCards = container?.querySelectorAll(".card.questionnaire-question");
43
-
44
- if (!container || !questionCards?.length) return;
45
-
46
- questionCards.forEach(card => {
47
- card.setAttribute("draggable", "true");
48
- card.setAttribute("role", "option");
49
- card.setAttribute("aria-grabbed", "false");
50
- });
51
-
52
- window.Decidim?.createSortableList?.("#questionnaire-questions-list");
53
- }
54
-
55
- // Initialize on load and when new options such as questions etc are added
56
- initializeSortable();
57
-
58
- const observer = new MutationObserver(() => {
59
- clearTimeout(observer.timer);
60
- observer.timer = setTimeout(initializeSortable, 500);
61
- });
62
-
63
- const container = document.querySelector("#questionnaire-questions-list");
64
- if (container) {
65
- observer.observe(container, { childList: true, subtree: true });
66
- }
67
38
  });
68
39
  </script>
@@ -1,6 +1,21 @@
1
1
  <% if current_user %>
2
2
  <% if @form.in_census? && @form.invalid? %>
3
3
  <%= cell "decidim/announcement", title: t(".invalid"), body: t(".authorization_options_invalid"), callout_class: "alert" %>
4
+ <% cell("decidim/authorization_modal", @form.authorization_status).verifications.each do |verification| %>
5
+ <div class="authorization-modal__verification">
6
+ <% verification[:messages].each do |msg| %>
7
+ <p><%= msg %></p>
8
+ <% end %>
9
+
10
+ <% if verification[:fields].present? %>
11
+ <ul>
12
+ <% verification[:fields].each do |field| %>
13
+ <li><%= field %></li>
14
+ <% end %>
15
+ </ul>
16
+ <% end %>
17
+ </div>
18
+ <% end %>
4
19
  <%= link_to t(".exit_button"), exit_path, class: "button button__secondary button__lg w-full mt-12" %>
5
20
  <%= render "decidim/elections/censuses/submit_button", form: form, disabled: true %>
6
21
  <% else %>
@@ -1,7 +1,8 @@
1
1
  <section class="layout-aside__section">
2
2
  <div class="election__aside-vote layout-aside__ctas-buttons" data-controller="sticky-buttons">
3
3
  <% if election.ongoing? %>
4
- <%= link_to t("vote_button", scope: "decidim.elections.elections.show"), new_election_vote_path(election), class: "button button__secondary button__lg" %>
4
+ <% button_key = voted_by_current_user?(election) ? "edit_vote_button" : "vote_button" %>
5
+ <%= link_to t(button_key, scope: "decidim.elections.elections.show"), new_election_vote_path(election), class: "button button__secondary button__lg" %>
5
6
  <% if voted_by_current_user?(election) %>
6
7
  <div class="election__aside-voted mt-6 hidden lg:block">
7
8
  <span><%= icon "check-line", class: "fill-success inline mr-2" %></span>
@@ -9,5 +9,9 @@
9
9
  <% question.response_options.each_with_index do |option, index| %>
10
10
  <%= render "decidim/elections/elections/vote_results_option", option: %>
11
11
  <% end %>
12
+ <div class="py-4 flex justify-between items-center">
13
+ <span><%= t("decidim.elections.elections.vote_results.total") %></span>
14
+ <span data-question-total-votes-text="<%= question.id %>"><%= t("total_votes", scope: "decidim.elections.elections.vote_results", count: question.total_votes) %></span>
15
+ </div>
12
16
  </div>
13
17
  </div>
@@ -1,3 +1,4 @@
1
+ <div class="voting-booth" data-question-status-url="<%= url_for(action: :show, id: question, format: :json) %>">
1
2
  <%= form_with url: url_for(action: :show, id: question), method: :patch, local: true do %>
2
3
  <%= question_title(question, :h1, class: "vote_booth-question_title") %>
3
4
  <div class="editor-content">
@@ -28,3 +29,4 @@
28
29
  </div>
29
30
  </div>
30
31
  <% end %>
32
+ </div>
@@ -4,5 +4,22 @@
4
4
  <h2 class="h2 text-center mb-6"><%= t(".title") %></h2>
5
5
  <p class="text-center"><%= t(".description") %></p>
6
6
  </div>
7
- <%= link_to t(".exit_button"), exit_path, class: "button button__secondary button__lg w-full mt-12" %>
7
+
8
+ <% editable_question = election.per_question? ? election.questions.enabled.unpublished_results.last : nil %>
9
+ <% exit_button_path = election.per_question? ? url_for(action: :receipt, exit: true) : exit_path %>
10
+ <% if editable_question.present? %>
11
+ <div class="vote-navigation w-full flex justify-between mt-8">
12
+ <%= link_to election_per_question_vote_path(election_id: election, id: editable_question),
13
+ class: "button button__lg button__transparent-secondary" do %>
14
+ <%= icon "edit-line", class: "fill-current mr-2" %>
15
+ <%= t(".edit_vote") %>
16
+ <% end %>
17
+
18
+ <div class="ml-auto">
19
+ <%= link_to t(".exit_button"), exit_button_path, class: "button button__lg button__secondary" %>
20
+ </div>
21
+ </div>
22
+ <% else %>
23
+ <%= link_to t(".exit_button"), exit_button_path, class: "button button__secondary button__lg w-full mt-12" %>
24
+ <% end %>
8
25
  </div>
@@ -80,6 +80,7 @@ ca-IT:
80
80
  questions_table:
81
81
  answer: Respostes
82
82
  percentage: Percentatge
83
+ total: Total
83
84
  votes: Vots
84
85
  votes_count:
85
86
  one: 1 vot
@@ -199,9 +200,6 @@ ca-IT:
199
200
  authorization_options_invalid: Malauradament, tot i que tens totes les autoritzacions necessàries, algunes no són vàlides per a aquestes eleccions.
200
201
  exit_button: Sortir de la cabina de votació
201
202
  invalid: No tens autorització per votar en aquestes eleccions.
202
- missing: El vostre compte d'usuària no disposa d'algunes de les autoritzacions necessàries. Revisa la informació i torna-ho a provar.
203
- not_authorized: 'L''autorització de "%{adapter}" no coincideix amb les condicions necessàries.'
204
- not_granted: 'L''autorització de "%{adapter}" no està concedida.'
205
203
  resume_with_method: Reprendre la verificació amb %{name}
206
204
  verify_with_method: Verificar amb %{name}
207
205
  token_csv:
@@ -246,6 +244,7 @@ ca-IT:
246
244
  title: Preguntes de l'elecció
247
245
  show:
248
246
  active_voting_until: 'Votació activa fins: %{end_date}'
247
+ edit_vote_button: Edita el vot
249
248
  vote_button: Votar
250
249
  voted: Ja has votat. Pots tornar a votar, això esborrarà el teu vot anterior i, només s'explicarà la teva última votació.
251
250
  votes_count:
@@ -258,6 +257,10 @@ ca-IT:
258
257
  per_question: Els resultats estan disponibles per pregunta. Pots veure els resultats de cada pregunta una vegada que s'habiliti la votació i es publiquin els resultats.
259
258
  real_time: Els resultats estan disponibles a temps real. Podeu veure els resultats mentre la votació està en curs.
260
259
  title: Resultats
260
+ total: 'TOTAL:'
261
+ total_votes:
262
+ one: 1 vot
263
+ other: "%{count} vots"
261
264
  models:
262
265
  election:
263
266
  fields:
@@ -303,6 +306,7 @@ ca-IT:
303
306
  next: Següent
304
307
  receipt:
305
308
  description: Pots tornar a votar en qualsevol moment mentre el període de votació estigui obert. El teu vot anterior serà substituït pel nou.
309
+ edit_vote: Edita el teu vot
306
310
  exit_button: Sortir de la cabina de votació
307
311
  title: El teu vot s'ha emès correctament
308
312
  metadata:
@@ -80,6 +80,7 @@ ca:
80
80
  questions_table:
81
81
  answer: Respostes
82
82
  percentage: Percentatge
83
+ total: Total
83
84
  votes: Vots
84
85
  votes_count:
85
86
  one: 1 vot
@@ -199,9 +200,6 @@ ca:
199
200
  authorization_options_invalid: Malauradament, tot i que tens totes les autoritzacions necessàries, algunes no són vàlides per a aquestes eleccions.
200
201
  exit_button: Sortir de la cabina de votació
201
202
  invalid: No tens autorització per votar en aquestes eleccions.
202
- missing: El vostre compte d'usuària no disposa d'algunes de les autoritzacions necessàries. Revisa la informació i torna-ho a provar.
203
- not_authorized: 'L''autorització de "%{adapter}" no coincideix amb les condicions necessàries.'
204
- not_granted: 'L''autorització de "%{adapter}" no està concedida.'
205
203
  resume_with_method: Reprendre la verificació amb %{name}
206
204
  verify_with_method: Verificar amb %{name}
207
205
  token_csv:
@@ -246,6 +244,7 @@ ca:
246
244
  title: Preguntes de l'elecció
247
245
  show:
248
246
  active_voting_until: 'Votació activa fins: %{end_date}'
247
+ edit_vote_button: Edita el vot
249
248
  vote_button: Votar
250
249
  voted: Ja has votat. Pots tornar a votar, això esborrarà el teu vot anterior i, només s'explicarà la teva última votació.
251
250
  votes_count:
@@ -258,6 +257,10 @@ ca:
258
257
  per_question: Els resultats estan disponibles per pregunta. Pots veure els resultats de cada pregunta una vegada que s'habiliti la votació i es publiquin els resultats.
259
258
  real_time: Els resultats estan disponibles a temps real. Podeu veure els resultats mentre la votació està en curs.
260
259
  title: Resultats
260
+ total: 'TOTAL:'
261
+ total_votes:
262
+ one: 1 vot
263
+ other: "%{count} vots"
261
264
  models:
262
265
  election:
263
266
  fields:
@@ -303,6 +306,7 @@ ca:
303
306
  next: Següent
304
307
  receipt:
305
308
  description: Pots tornar a votar en qualsevol moment mentre el període de votació estigui obert. El teu vot anterior serà substituït pel nou.
309
+ edit_vote: Edita el teu vot
306
310
  exit_button: Sortir de la cabina de votació
307
311
  title: El teu vot s'ha emès correctament
308
312
  metadata:
@@ -37,6 +37,7 @@ cs:
37
37
  census:
38
38
  edit:
39
39
  created_at: Vytvořeno v
40
+ identifier: Identifikátor uživatele
40
41
  dashboard:
41
42
  calendar:
42
43
  end_at: 'Čas ukončení:'
@@ -72,6 +73,8 @@ cs:
72
73
  start_question_button: Povolit hlasování
73
74
  title: Výsledky
74
75
  status:
76
+ results_availability:
77
+ real_time: Výsledky jsou dostupné v reálném čase
75
78
  title: Stav voleb
76
79
  elections:
77
80
  create:
@@ -114,6 +117,9 @@ cs:
114
117
  edit_questions:
115
118
  new_question: Nová otázka
116
119
  title: Otázky
120
+ form:
121
+ errors:
122
+ at_least_one_question: Je vyžadována alespoň jedna otázka.
117
123
  statuses:
118
124
  publish_results:
119
125
  invalid: Při publikování výsledků došlo k chybě.
@@ -133,7 +139,6 @@ cs:
133
139
  internal_users_form:
134
140
  authorization_options_invalid: Bohužel, i když máte všechna požadovaná oprávnění, některé z nich pro tyto volby neplatí.
135
141
  invalid: V těchto volbách nemáte oprávnění volit.
136
- missing: Vašemu uživateli chybí některá požadovaná autorizace. Zkontrolujte prosím informace a zkuste to znovu.
137
142
  resume_with_method: Pokračovat v ověřování pomocí %{name}
138
143
  verify_with_method: Ověřit pomocí %{name}
139
144
  token_csv: