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.
- checksums.yaml +4 -4
- data/app/commands/decidim/elections/admin/update_election.rb +3 -3
- data/app/controllers/concerns/decidim/elections/uses_votes_booth.rb +8 -2
- data/app/controllers/decidim/elections/per_question_votes_controller.rb +28 -1
- data/app/forms/decidim/elections/admin/election_form.rb +10 -0
- data/app/forms/decidim/elections/censuses/internal_users_form.rb +7 -13
- data/app/models/decidim/elections/election.rb +4 -8
- data/app/packs/entrypoints/decidim_elections.js +1 -0
- data/app/packs/src/decidim/elections/live_results_update.js +22 -0
- data/app/packs/src/decidim/elections/question_status_checker.js +42 -0
- data/app/permissions/decidim/elections/admin/permissions.rb +3 -5
- data/app/presenters/decidim/elections/election_presenter.rb +8 -3
- data/app/views/decidim/elections/admin/dashboard/_questions_with_results.html.erb +5 -0
- data/app/views/decidim/elections/admin/elections/_election-tr.html.erb +1 -1
- data/app/views/decidim/elections/admin/elections/_form.html.erb +1 -1
- data/app/views/decidim/elections/admin/questions/_form.html.erb +1 -30
- data/app/views/decidim/elections/censuses/_internal_users_form.html.erb +15 -0
- data/app/views/decidim/elections/elections/_election_aside.html.erb +2 -1
- data/app/views/decidim/elections/elections/_vote_results_question.html.erb +4 -0
- data/app/views/decidim/elections/per_question_votes/show.html.erb +2 -0
- data/app/views/decidim/elections/votes/receipt.html.erb +18 -1
- data/config/locales/ca-IT.yml +7 -3
- data/config/locales/ca.yml +7 -3
- data/config/locales/cs.yml +6 -1
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +8 -4
- data/config/locales/es-MX.yml +7 -3
- data/config/locales/es-PY.yml +7 -3
- data/config/locales/es.yml +7 -3
- data/config/locales/eu.yml +8 -4
- data/config/locales/fi-plain.yml +7 -3
- data/config/locales/fi.yml +7 -3
- data/config/locales/fr-CA.yml +8 -4
- data/config/locales/fr.yml +8 -4
- data/config/locales/ja.yml +6 -3
- data/config/locales/pt-BR.yml +194 -0
- data/config/locales/sv.yml +10 -3
- data/lib/decidim/elections/admin_engine.rb +4 -4
- data/lib/decidim/elections/test/per_question_vote_examples.rb +105 -3
- data/lib/decidim/elections/test/vote_examples.rb +5 -3
- data/lib/decidim/elections/version.rb +1 -1
- metadata +15 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3314659cca0234480f9fbca9edd5ebd51d55f465d8e66608aadf327c04101d73
|
|
4
|
+
data.tar.gz: 3bbb9c90945c6c7e14e37e993d1095ce69e78ef7508f9790186a38449b5f36d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
20
|
+
election.started? ? started_election_attributes : not_started_election_attributes
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def
|
|
23
|
+
def started_election_attributes
|
|
24
24
|
{ description: parsed_description }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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? &&
|
|
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
|
-
|
|
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.
|
|
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 %>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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>
|
data/config/locales/ca-IT.yml
CHANGED
|
@@ -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:
|
data/config/locales/ca.yml
CHANGED
|
@@ -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:
|
data/config/locales/cs.yml
CHANGED
|
@@ -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:
|