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.
- 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/helpers/decidim/elections/application_helper.rb +12 -0
- 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/packs/stylesheets/decidim/elections/elections.scss +4 -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/census/edit.html.erb +15 -13
- data/app/views/decidim/elections/admin/dashboard/_questions.html.erb +1 -0
- data/app/views/decidim/elections/admin/dashboard/_questions_with_results.html.erb +6 -0
- data/app/views/decidim/elections/admin/elections/_election-tr.html.erb +1 -1
- data/app/views/decidim/elections/admin/elections/_form.html.erb +3 -3
- data/app/views/decidim/elections/admin/elections/dashboard.html.erb +4 -4
- data/app/views/decidim/elections/admin/elections/new.html.erb +1 -1
- data/app/views/decidim/elections/admin/questions/_form.html.erb +1 -30
- data/app/views/decidim/elections/admin/questions/edit_questions.html.erb +2 -2
- 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 +6 -1
- data/app/views/decidim/elections/votes/receipt.html.erb +18 -1
- data/app/views/decidim/elections/votes/show.html.erb +4 -1
- data/config/locales/ca-IT.yml +7 -4
- data/config/locales/ca.yml +7 -4
- data/config/locales/cs.yml +28 -0
- data/config/locales/de.yml +4 -0
- data/config/locales/en.yml +8 -5
- data/config/locales/es-MX.yml +7 -4
- data/config/locales/es-PY.yml +7 -4
- data/config/locales/es.yml +7 -4
- data/config/locales/eu.yml +9 -6
- data/config/locales/fi-plain.yml +7 -4
- data/config/locales/fi.yml +7 -4
- data/config/locales/fr-CA.yml +16 -4
- data/config/locales/fr.yml +16 -4
- data/config/locales/ja.yml +6 -4
- data/config/locales/pt-BR.yml +194 -0
- data/config/locales/sv.yml +10 -4
- data/lib/decidim/elections/admin_engine.rb +4 -4
- data/lib/decidim/elections/engine.rb +6 -0
- data/lib/decidim/elections/test/per_question_vote_examples.rb +114 -3
- data/lib/decidim/elections/test/vote_examples.rb +7 -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
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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(".
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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.
|
|
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
|
|
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(".
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|