decidim-elections 0.31.0.rc2 → 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 (51) 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/helpers/decidim/elections/application_helper.rb +12 -0
  8. data/app/models/decidim/elections/election.rb +4 -8
  9. data/app/packs/entrypoints/decidim_elections.js +1 -0
  10. data/app/packs/src/decidim/elections/live_results_update.js +22 -0
  11. data/app/packs/src/decidim/elections/question_status_checker.js +42 -0
  12. data/app/packs/stylesheets/decidim/elections/elections.scss +4 -0
  13. data/app/permissions/decidim/elections/admin/permissions.rb +3 -5
  14. data/app/presenters/decidim/elections/election_presenter.rb +8 -3
  15. data/app/views/decidim/elections/admin/census/edit.html.erb +15 -13
  16. data/app/views/decidim/elections/admin/dashboard/_questions.html.erb +1 -0
  17. data/app/views/decidim/elections/admin/dashboard/_questions_with_results.html.erb +6 -0
  18. data/app/views/decidim/elections/admin/elections/_election-tr.html.erb +1 -1
  19. data/app/views/decidim/elections/admin/elections/_form.html.erb +3 -3
  20. data/app/views/decidim/elections/admin/elections/dashboard.html.erb +4 -4
  21. data/app/views/decidim/elections/admin/elections/new.html.erb +1 -1
  22. data/app/views/decidim/elections/admin/questions/_form.html.erb +1 -30
  23. data/app/views/decidim/elections/admin/questions/edit_questions.html.erb +2 -2
  24. data/app/views/decidim/elections/censuses/_internal_users_form.html.erb +15 -0
  25. data/app/views/decidim/elections/elections/_election_aside.html.erb +2 -1
  26. data/app/views/decidim/elections/elections/_vote_results_question.html.erb +4 -0
  27. data/app/views/decidim/elections/per_question_votes/show.html.erb +6 -1
  28. data/app/views/decidim/elections/votes/receipt.html.erb +18 -1
  29. data/app/views/decidim/elections/votes/show.html.erb +4 -1
  30. data/config/locales/ca-IT.yml +7 -4
  31. data/config/locales/ca.yml +7 -4
  32. data/config/locales/cs.yml +28 -0
  33. data/config/locales/de.yml +4 -0
  34. data/config/locales/en.yml +8 -5
  35. data/config/locales/es-MX.yml +7 -4
  36. data/config/locales/es-PY.yml +7 -4
  37. data/config/locales/es.yml +7 -4
  38. data/config/locales/eu.yml +9 -6
  39. data/config/locales/fi-plain.yml +7 -4
  40. data/config/locales/fi.yml +7 -4
  41. data/config/locales/fr-CA.yml +16 -4
  42. data/config/locales/fr.yml +16 -4
  43. data/config/locales/ja.yml +6 -4
  44. data/config/locales/pt-BR.yml +194 -0
  45. data/config/locales/sv.yml +10 -4
  46. data/lib/decidim/elections/admin_engine.rb +4 -4
  47. data/lib/decidim/elections/engine.rb +6 -0
  48. data/lib/decidim/elections/test/per_question_vote_examples.rb +114 -3
  49. data/lib/decidim/elections/test/vote_examples.rb +7 -3
  50. data/lib/decidim/elections/version.rb +1 -1
  51. metadata +15 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1350982617f9cf2b5417179740fe67d319fd01e74c85fbcc823f092d78b23923
4
- data.tar.gz: 29788cfe9ee20c68fbbd6739baee65968c98c1ed3daf344e11634ba3f458687e
3
+ metadata.gz: 3314659cca0234480f9fbca9edd5ebd51d55f465d8e66608aadf327c04101d73
4
+ data.tar.gz: 3bbb9c90945c6c7e14e37e993d1095ce69e78ef7508f9790186a38449b5f36d5
5
5
  SHA512:
6
- metadata.gz: 3535497567d41f0d365331a63ffb3aa2f751bcef39460de3f9c3ab377f64e0be9011f85748b819f4910141bdcfbd5ff506b96d11ba3d52d71175571381b80f91
7
- data.tar.gz: 07cfca093845a3378687f949072ac9a16fcace3ce766e1bbe6be70922db0dc04b8380c3c44fa67d867802766ef5cf1800dd164d9937889ec8c375be1140e3526
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
@@ -43,6 +43,18 @@ module Decidim
43
43
  end
44
44
  end
45
45
 
46
+ def render_question_description(question)
47
+ description = translated_attribute(question.description)
48
+ return if description.blank?
49
+
50
+ sanitized = decidim_sanitize_admin(description)
51
+ if rich_text_editor_in_public_views?
52
+ Decidim::ContentProcessor.render_without_format(sanitized).html_safe
53
+ else
54
+ Decidim::ContentProcessor.render(sanitized, "div")
55
+ end
56
+ end
57
+
46
58
  def selected_response_option_id(question)
47
59
  session.dig(:votes_buffer, question.id.to_s, "response_option_id")&.to_i
48
60
  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
+ });
@@ -89,3 +89,7 @@
89
89
  }
90
90
  }
91
91
  }
92
+
93
+ .vote_booth-question_title {
94
+ @apply h3 mb-4;
95
+ }
@@ -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
@@ -2,33 +2,35 @@
2
2
 
3
3
  <div class="item_show__header form-defaults border-none">
4
4
  <h1 class="item_show__header-title">
5
- <%= t(".title") %>
5
+ <%= t("decidim.elections.actions.edit") %>
6
6
  </h1>
7
+ </div>
8
+
9
+ <%= render "decidim/elections/admin/elections/tabs_menu" %>
10
+
11
+ <div class="form-defaults my-8 flex justify-end">
7
12
  <%= select_tag "census_manifest",
8
13
  options_for_select(census_manifests.to_h { |manifest| [manifest.label, manifest.name] }, selected: election.census&.name),
9
14
  include_blank: t(".choose_census"),
10
15
  id: "census-manifest-selector" %>
11
-
12
16
  </div>
13
17
 
14
- <%= render "decidim/elections/admin/elections/tabs_menu" %>
15
-
16
- <% if election.census_ready? %>
17
- <%
18
- census_ready = t("decidim.elections.censuses.census_ready_html", election_title: decidim_sanitize_translated(election.title))
19
- census_count = t("decidim.elections.censuses.census_size_html", count: census_count(election))
20
- %>
18
+ <% if election.census_ready? %>
19
+ <%
20
+ census_ready = t("decidim.elections.censuses.census_ready_html", election_title: decidim_sanitize_translated(election.title))
21
+ census_count = t("decidim.elections.censuses.census_size_html", count: census_count(election))
22
+ %>
21
23
  <%= cell "decidim/announcement", "#{census_ready}<br>#{census_count}", callout_class: "success" %>
22
- <% end %>
24
+ <% end %>
25
+
23
26
  <div class="card-section census-form form-defaults 2xl:mr-80">
24
27
  <% if @form && election.census&.admin_form_partial %>
25
28
  <%= decidim_form_for(@form, url: election_census_path(election, manifest: election.census&.name), method: :patch, multipart: true, html: { id: "census-election-form" }) do |f| %>
26
29
  <%= render partial: election.census.admin_form_partial, locals: { form: f } %>
27
30
  <% end %>
28
31
  <% end %>
29
- <div class="border-t border-gray-3 my-4">
30
- <%= render partial: "decidim/elections/admin/census/preview", locals: { election: } if preview_users(election).present? %>
31
- </div>
32
+
33
+ <%= render partial: "decidim/elections/admin/census/preview", locals: { election: } if preview_users(election).present? %>
32
34
  </div>
33
35
 
34
36
  <div class="item__edit-sticky">
@@ -19,6 +19,7 @@
19
19
  <%= translated_attribute(question.body) %>
20
20
  <small class="font-normal ml-4"><%= t("decidim.forms.question_types.#{question.question_type}") %></small>
21
21
  </h3>
22
+ <div><%= decidim_sanitize_admin translated_attribute(question.description) %></div>
22
23
  <h3 class="h6 mt-4 mb-2"><%= t("decidim.elections.admin.dashboard.questions.labels.answers") %>:</h3>
23
24
  <ul>
24
25
  <% question.response_options.each_with_index do |option, opt_index| %>
@@ -15,6 +15,7 @@
15
15
  </div>
16
16
  <% end %>
17
17
  </h3>
18
+ <div><%= decidim_sanitize_admin translated_attribute(question.description) %></div>
18
19
  </div>
19
20
  <div class="row column">
20
21
  <div class="table-scroll mb-4">
@@ -34,6 +35,11 @@
34
35
  <td data-option-votes-percent-text="<%= question.id %>,<%= option.id %>"><%= number_to_percentage(option.votes_percent, precision: 1) %></td>
35
36
  </tr>
36
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>
37
43
  </tbody>
38
44
  </table>
39
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">
@@ -55,11 +55,11 @@
55
55
  <div class="row column election-fields--time">
56
56
  <div class="md:flex w-100">
57
57
  <div class="row column election_start_time">
58
- <%= form.datetime_field :start_at, label: t("decidim.elections.admin.elections.form.start_at") %>
58
+ <%= form.datetime_field :start_at, label: t("decidim.elections.admin.elections.form.start_at"), disabled: disable_fields %>
59
59
  <%= form.hidden_field :start_at, value: election.start_at if disable_fields %>
60
60
  </div>
61
61
  <div class="row column">
62
- <%= form.datetime_field :end_at, label: t("decidim.elections.admin.elections.form.end_at") %>
62
+ <%= form.datetime_field :end_at, label: t("decidim.elections.admin.elections.form.end_at"), disabled: disable_fields %>
63
63
  <%= form.hidden_field :end_at, value: election.end_at if disable_fields %>
64
64
  </div>
65
65
  </div>
@@ -5,15 +5,15 @@
5
5
 
6
6
  <div class="item_show__header" style="border-bottom: none;">
7
7
  <h1 class="item_show__header-title">
8
- <%= t(".title") %>
8
+ <%= t("decidim.elections.actions.edit") %>
9
9
  </h1>
10
10
 
11
11
  <div>
12
- <%= link_to resource_locator(election).path, class: "button button__xs button__transparent-secondary", target: :blank, data: { "external-link": false } do %>
13
- <%= icon "eye-line" %>
12
+ <%= link_to resource_locator(election).path, class: "button button__xs button__transparent-secondary flex flex-row items-center gap-2", target: :blank, data: { "external-link": false } do %>
13
+ <%= icon "eye-line", class: "inline-block" %>
14
14
  <%# i18n-tasks-use t("decidim.elections.actions.view") %>
15
15
  <%# i18n-tasks-use t("decidim.elections.actions.preview") %>
16
- <%= t(election.published? ? "view" : "preview", scope: "decidim.elections.actions") %>
16
+ <span class="whitespace-nowrap"><%= t(election.published? ? "view" : "preview", scope: "decidim.elections.actions") %></span>
17
17
  <% end %>
18
18
  </div>
19
19
  </div>
@@ -3,7 +3,7 @@
3
3
  <% append_javascript_pack_tag "decidim_elections_admin" %>
4
4
  <% append_stylesheet_pack_tag "decidim_elections_admin" %>
5
5
 
6
- <div class="item_show__header">
6
+ <div class="item_show__header border-none">
7
7
  <h1 class="item_show__header-title">
8
8
  <%= t(".title") %>
9
9
  </h1>
@@ -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>
@@ -3,8 +3,7 @@
3
3
  <div class="questionnaire-questions">
4
4
  <div class="item_show__header" style="border-bottom: none;">
5
5
  <h1 class="item_show__header-title">
6
- <%= t(".title") %>
7
- <button class="button button__sm button__transparent-secondary add-question"><%= t("add_question", scope: "decidim.forms.admin.questionnaires.form") %></button>
6
+ <%= t("decidim.elections.actions.edit") %>
8
7
  </h1>
9
8
  </div>
10
9
 
@@ -26,5 +25,6 @@
26
25
  </div>
27
26
  </div>
28
27
  </div>
28
+ <button class="button button__sm button__transparent-secondary add-question mt-1"><%= t("add_question", scope: "decidim.forms.admin.questionnaires.form") %></button>
29
29
  </div>
30
30
  </div>
@@ -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>