decidim-elections 0.31.5 → 0.32.0.rc2
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/README.md +7 -1
- data/app/commands/decidim/elections/admin/process_census.rb +1 -1
- data/app/commands/decidim/elections/admin/update_election.rb +1 -0
- data/app/commands/decidim/elections/admin/update_election_status.rb +1 -1
- data/app/commands/decidim/elections/admin/update_questions.rb +1 -0
- data/app/commands/decidim/elections/cast_votes.rb +3 -3
- data/app/controllers/concerns/decidim/elections/uses_census_access.rb +39 -0
- data/app/controllers/concerns/decidim/elections/uses_votes_booth.rb +4 -26
- data/app/controllers/decidim/elections/admin/census_controller.rb +5 -5
- data/app/controllers/decidim/elections/admin/elections_controller.rb +20 -9
- data/app/controllers/decidim/elections/admin/questions_controller.rb +1 -1
- data/app/controllers/decidim/elections/census_checks_controller.rb +58 -0
- data/app/controllers/decidim/elections/elections_controller.rb +11 -1
- data/app/controllers/decidim/elections/votes_controller.rb +8 -0
- data/app/forms/decidim/elections/admin/question_form.rb +6 -0
- data/app/forms/decidim/elections/censuses/internal_users_form.rb +1 -1
- data/app/helpers/decidim/elections/application_helper.rb +8 -3
- data/app/models/decidim/elections/question.rb +1 -0
- data/app/models/decidim/elections/vote.rb +2 -1
- data/app/models/decidim/elections/voter.rb +1 -0
- data/app/packs/entrypoints/decidim_elections.js +2 -0
- data/app/packs/entrypoints/decidim_elections_admin.js +1 -0
- data/app/packs/src/decidim/elections/admin/census_check_toggle.js +44 -0
- data/app/packs/src/decidim/elections/census_check_visibility.js +52 -0
- data/app/packs/src/decidim/elections/elections.js +32 -0
- data/app/permissions/decidim/elections/permissions.rb +13 -0
- data/app/presenters/decidim/elections/election_presenter.rb +3 -1
- data/app/views/decidim/elections/admin/census/edit.html.erb +2 -1
- data/app/views/decidim/elections/admin/censuses/_internal_users_form.html.erb +1 -1
- data/app/views/decidim/elections/admin/dashboard/_status.html.erb +12 -0
- data/app/views/decidim/elections/admin/elections/_election-tr.html.erb +3 -3
- data/app/views/decidim/elections/admin/questions/_question.html.erb +11 -0
- data/app/views/decidim/elections/census_checks/show.html.erb +8 -0
- data/app/views/decidim/elections/censuses/_internal_users_form.html.erb +2 -2
- data/app/views/decidim/elections/censuses/_token_csv_form.html.erb +1 -1
- data/app/views/decidim/elections/elections/_election_aside.html.erb +8 -0
- data/app/views/decidim/elections/elections/index.html.erb +1 -1
- data/app/views/decidim/elections/elections/show.html.erb +1 -1
- data/app/views/decidim/elections/votes/show.html.erb +6 -2
- data/config/locales/ca-IT.yml +16 -0
- data/config/locales/ca.yml +16 -0
- data/config/locales/cs.yml +7 -0
- data/config/locales/en.yml +16 -0
- data/config/locales/es-MX.yml +16 -0
- data/config/locales/es-PY.yml +16 -0
- data/config/locales/es.yml +16 -0
- data/config/locales/eu.yml +16 -0
- data/config/locales/fi-plain.yml +16 -0
- data/config/locales/fi.yml +16 -0
- data/config/locales/fr-CA.yml +16 -0
- data/config/locales/fr.yml +16 -0
- data/config/locales/ja.yml +16 -0
- data/config/locales/pt-BR.yml +16 -0
- data/config/locales/sk.yml +16 -0
- data/config/locales/sv.yml +15 -0
- data/db/migrate/20251007113417_add_max_choices_to_decidim_elections_questions.rb +7 -0
- data/db/migrate/20251020130630_add_allow_census_check_before_start_to_decidim_elections_elections.rb +7 -0
- data/decidim-elections.gemspec +9 -10
- data/lib/decidim/elections/admin_engine.rb +1 -0
- data/lib/decidim/elections/component.rb +0 -4
- data/lib/decidim/elections/engine.rb +2 -1
- data/lib/decidim/elections/seeds.rb +1 -1
- data/lib/decidim/elections/test/factories.rb +7 -3
- data/lib/decidim/elections/test/vote_controller_examples.rb +6 -6
- data/lib/decidim/elections/test/vote_examples.rb +4 -4
- data/lib/decidim/elections/version.rb +1 -1
- metadata +24 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ed28042a5ba0fce7e077a20442c88ba8aa8d8e3521fc4e2f9f6a8ccda083757
|
|
4
|
+
data.tar.gz: 297dcd5aa0f624246f8fc9c0e657b7cf6c734e299f3da9ec5495fcdaa29e2b87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f0cac1305197050665c4f751d57643818f76b99966e180993a78044fb367f75c71b2dab404fff2a5ae247465d1c0b6c1fdaa36fb553693e2ad675eb50d6ebb7
|
|
7
|
+
data.tar.gz: 3bb3edb2068ea51e0e24933e7de96637241d7b1a83b1766c3ed8ba6dd5801b6ee072df2acddbabca53cb965880a247e9cc2e8d42b5526d1c3394d5acfd942d0e
|
data/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# decidim-elections
|
|
2
2
|
|
|
3
3
|
The Decidim::Elections is a component that allows users to setup non-cryptographic elections. Elections are basically polls/surveys with census access management built-in. This allows registered or non-registered users to directly participate in them.
|
|
4
4
|
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Elections will be available as a Component for a Participatory Space.
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
In order to install use this module, you need at least Decidim 0.31 to be installed.
|
|
@@ -10,6 +14,8 @@ To install this module, run in your console:
|
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
16
|
bundle add decidim-elections
|
|
17
|
+
bundle exec rails decidim:upgrade
|
|
18
|
+
bundle exec rails db:migrate
|
|
13
19
|
```
|
|
14
20
|
|
|
15
21
|
## Contributing
|
|
@@ -40,11 +40,11 @@ module Decidim
|
|
|
40
40
|
voted_questions.each do |question, responses|
|
|
41
41
|
raise StandardError, "No responses for question #{question.id}" if responses.blank?
|
|
42
42
|
|
|
43
|
-
question.votes.where(voter_uid:
|
|
43
|
+
question.votes.where(voter_uid:).destroy_all
|
|
44
44
|
responses.each do |response_option|
|
|
45
45
|
question.votes.create!(
|
|
46
|
-
voter_uid
|
|
47
|
-
response_option:
|
|
46
|
+
voter_uid:,
|
|
47
|
+
response_option:
|
|
48
48
|
)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Elections
|
|
5
|
+
module UsesCensusAccess
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
helper_method :exit_path, :election, :session_authenticated?, :session_attributes, :voter_uid
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def election
|
|
15
|
+
@election ||= Election.where(component: current_component).published.find(params[:election_id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def session_authenticated?
|
|
19
|
+
@session_authenticated ||= election.census.valid_user?(election, session_attributes, current_user:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def voter_uid
|
|
23
|
+
@voter_uid ||= election.census.voter_uid(election, session_attributes, current_user:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def session_attributes
|
|
27
|
+
session[:session_attributes] ||= {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exit_path
|
|
31
|
+
@exit_path ||= if allowed_to?(:read, :election, election:)
|
|
32
|
+
election_path(election)
|
|
33
|
+
else
|
|
34
|
+
elections_path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -7,8 +7,10 @@ module Decidim
|
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
9
|
included do
|
|
10
|
+
include UsesCensusAccess
|
|
11
|
+
|
|
10
12
|
layout "decidim/election_booth"
|
|
11
|
-
helper_method :
|
|
13
|
+
helper_method :questions, :question, :response_chosen?, :votes_buffer
|
|
12
14
|
|
|
13
15
|
before_action except: [:new, :create, :receipt] do
|
|
14
16
|
next if session_authenticated?
|
|
@@ -64,10 +66,6 @@ module Decidim
|
|
|
64
66
|
|
|
65
67
|
private
|
|
66
68
|
|
|
67
|
-
def election
|
|
68
|
-
@election ||= Election.where(component: current_component).published.find(params[:election_id])
|
|
69
|
-
end
|
|
70
|
-
|
|
71
69
|
def questions
|
|
72
70
|
@questions ||= election.questions
|
|
73
71
|
end
|
|
@@ -76,22 +74,10 @@ module Decidim
|
|
|
76
74
|
@question ||= questions.find_by(id: params[:id]) || questions.first
|
|
77
75
|
end
|
|
78
76
|
|
|
79
|
-
def session_authenticated?
|
|
80
|
-
@session_authenticated ||= election.census.valid_user?(election, session_attributes, current_user:)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def voter_uid
|
|
84
|
-
@voter_uid ||= election.census.voter_uid(election, session_attributes, current_user:)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
77
|
def votes_buffer
|
|
88
78
|
session[:votes_buffer] ||= {}
|
|
89
79
|
end
|
|
90
80
|
|
|
91
|
-
def session_attributes
|
|
92
|
-
session[:session_attributes] ||= {}
|
|
93
|
-
end
|
|
94
|
-
|
|
95
81
|
def response_chosen?(response_option)
|
|
96
82
|
response_ids = if votes_buffer.has_key?(question.id.to_s)
|
|
97
83
|
votes_buffer[question.id.to_s]
|
|
@@ -105,17 +91,9 @@ module Decidim
|
|
|
105
91
|
|
|
106
92
|
def previous_responses
|
|
107
93
|
@previous_responses ||= election.questions.to_h do |question|
|
|
108
|
-
[question.id.to_s, question.votes.where(voter_uid:
|
|
94
|
+
[question.id.to_s, question.votes.where(voter_uid:).pluck(:response_option_id).map(&:to_s)]
|
|
109
95
|
end
|
|
110
96
|
end
|
|
111
|
-
|
|
112
|
-
def exit_path
|
|
113
|
-
@exit_path ||= if allowed_to?(:read, :election, election:)
|
|
114
|
-
election_path(election)
|
|
115
|
-
else
|
|
116
|
-
elections_path
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
97
|
end
|
|
120
98
|
end
|
|
121
99
|
end
|
|
@@ -11,15 +11,15 @@ module Decidim
|
|
|
11
11
|
before_action :set_census_manifest, only: [:edit, :update]
|
|
12
12
|
|
|
13
13
|
def edit
|
|
14
|
-
enforce_permission_to
|
|
14
|
+
enforce_permission_to(:update, :census, election:)
|
|
15
15
|
|
|
16
|
-
@form = form(election.census.admin_form.constantize).from_params(election.census_settings, election:
|
|
16
|
+
@form = form(election.census.admin_form.constantize).from_params(election.census_settings, election:) if election.census && election.census.admin_form.present?
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def update
|
|
20
|
-
enforce_permission_to
|
|
20
|
+
enforce_permission_to(:update, :census, election:)
|
|
21
21
|
|
|
22
|
-
@form = form(election.census.admin_form.constantize).from_params(params, election:
|
|
22
|
+
@form = form(election.census.admin_form.constantize).from_params(params, election:) if election.census.admin_form.present?
|
|
23
23
|
|
|
24
24
|
ProcessCensus.call(@form, election) do
|
|
25
25
|
on(:ok) do
|
|
@@ -28,7 +28,7 @@ module Decidim
|
|
|
28
28
|
end
|
|
29
29
|
on(:invalid) do
|
|
30
30
|
flash[:alert] = t("decidim.elections.admin.census.update.error")
|
|
31
|
-
render :edit, status: :
|
|
31
|
+
render :edit, status: :unprocessable_content
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -35,7 +35,7 @@ module Decidim
|
|
|
35
35
|
|
|
36
36
|
on(:invalid) do
|
|
37
37
|
flash.now[:alert] = I18n.t("elections.create.invalid", scope: "decidim.elections.admin")
|
|
38
|
-
render action: "new", status: :
|
|
38
|
+
render action: "new", status: :unprocessable_content
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -46,7 +46,7 @@ module Decidim
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def update
|
|
49
|
-
enforce_permission_to
|
|
49
|
+
enforce_permission_to(:update, :election, election:)
|
|
50
50
|
|
|
51
51
|
@form = form(Decidim::Elections::Admin::ElectionForm).from_params(params, current_component:, election:)
|
|
52
52
|
|
|
@@ -58,13 +58,13 @@ module Decidim
|
|
|
58
58
|
|
|
59
59
|
on(:invalid) do
|
|
60
60
|
flash.now[:alert] = I18n.t("elections.update.invalid", scope: "decidim.elections.admin")
|
|
61
|
-
render action: "edit", status: :
|
|
61
|
+
render action: "edit", status: :unprocessable_content
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def publish
|
|
67
|
-
enforce_permission_to
|
|
67
|
+
enforce_permission_to(:publish, :election, election:)
|
|
68
68
|
|
|
69
69
|
PublishElection.call(election, current_user) do
|
|
70
70
|
on(:ok) do
|
|
@@ -74,13 +74,13 @@ module Decidim
|
|
|
74
74
|
|
|
75
75
|
on(:invalid) do
|
|
76
76
|
flash.now[:alert] = I18n.t("elections.publish.invalid", scope: "decidim.elections.admin")
|
|
77
|
-
render action: "index", status: :
|
|
77
|
+
render action: "index", status: :unprocessable_content
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def unpublish
|
|
83
|
-
enforce_permission_to
|
|
83
|
+
enforce_permission_to(:unpublish, :election, election:)
|
|
84
84
|
|
|
85
85
|
Decidim::Elections::Admin::UnpublishElection.call(election, current_user) do
|
|
86
86
|
on(:ok) do
|
|
@@ -90,13 +90,13 @@ module Decidim
|
|
|
90
90
|
|
|
91
91
|
on(:invalid) do
|
|
92
92
|
flash.now[:alert] = I18n.t("elections.unpublish.invalid", scope: "decidim.elections.admin")
|
|
93
|
-
render action: "index", status: :
|
|
93
|
+
render action: "index", status: :unprocessable_content
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
def dashboard
|
|
99
|
-
enforce_permission_to
|
|
99
|
+
enforce_permission_to(:dashboard, :election, election:)
|
|
100
100
|
|
|
101
101
|
respond_to do |format|
|
|
102
102
|
format.html { render :dashboard }
|
|
@@ -107,7 +107,7 @@ module Decidim
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def update_status
|
|
110
|
-
enforce_permission_to
|
|
110
|
+
enforce_permission_to(:update, :election, election:)
|
|
111
111
|
|
|
112
112
|
status_action = params[:status_action]
|
|
113
113
|
UpdateElectionStatus.call(status_action, election) do
|
|
@@ -122,6 +122,17 @@ module Decidim
|
|
|
122
122
|
redirect_to dashboard_election_path(election)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
def toggle_census_check
|
|
126
|
+
enforce_permission_to(:update, :election, election:)
|
|
127
|
+
|
|
128
|
+
value = ActiveModel::Type::Boolean.new.cast(params[:allow_census_check_before_start])
|
|
129
|
+
election.update!(allow_census_check_before_start: value)
|
|
130
|
+
|
|
131
|
+
render json: { success: true, allow_census_check_before_start: election.allow_census_check_before_start }
|
|
132
|
+
rescue StandardError
|
|
133
|
+
render json: { success: false, error: I18n.t("elections.toggle_census_check.error", scope: "decidim.elections.admin") }, status: :unprocessable_content
|
|
134
|
+
end
|
|
135
|
+
|
|
125
136
|
private
|
|
126
137
|
|
|
127
138
|
def per_question_waiting?
|
|
@@ -29,7 +29,7 @@ module Decidim
|
|
|
29
29
|
|
|
30
30
|
on(:invalid) do
|
|
31
31
|
flash.now[:alert] = I18n.t("update.invalid", scope: "decidim.elections.admin.questions")
|
|
32
|
-
render :edit_questions, status: :
|
|
32
|
+
render :edit_questions, status: :unprocessable_content
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Elections
|
|
5
|
+
# Allows participants to verify they belong to an election census before voting starts.
|
|
6
|
+
class CensusChecksController < Decidim::Elections::ApplicationController
|
|
7
|
+
include UsesCensusAccess
|
|
8
|
+
|
|
9
|
+
layout "decidim/election_booth"
|
|
10
|
+
|
|
11
|
+
before_action :redirect_if_authenticated, only: :new
|
|
12
|
+
before_action :ensure_session_authenticated!, only: :show
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
enforce_permission_to(:create, :census_check, election:)
|
|
16
|
+
|
|
17
|
+
@form = election.census.form_instance({}, election:, current_user:)
|
|
18
|
+
render "decidim/elections/votes/new"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
enforce_permission_to(:create, :census_check, election:)
|
|
23
|
+
|
|
24
|
+
@form = election.census.form_instance(params, election:, current_user:)
|
|
25
|
+
if @form.valid?
|
|
26
|
+
session[:session_attributes] = @form.attributes
|
|
27
|
+
redirect_to election_census_check_path(election)
|
|
28
|
+
else
|
|
29
|
+
flash[:alert] = @form.errors.full_messages.join("<br>").presence || t("failed", scope: "decidim.elections.votes.check_census")
|
|
30
|
+
redirect_to new_election_census_check_path(election)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def show
|
|
35
|
+
enforce_permission_to(:read, :census_check, election:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Allows admins to access unpublished elections for preview.
|
|
41
|
+
def election
|
|
42
|
+
@election ||= Election.where(component: current_component)
|
|
43
|
+
.then { |scope| current_user&.admin? ? scope : scope.published }
|
|
44
|
+
.find(params[:election_id])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def redirect_if_authenticated
|
|
48
|
+
redirect_to election_census_check_path(election) if session_authenticated?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ensure_session_authenticated!
|
|
52
|
+
return if session_authenticated?
|
|
53
|
+
|
|
54
|
+
redirect_to new_election_census_check_path(election), alert: t("decidim.elections.votes.check_census.failed")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -30,7 +30,7 @@ module Decidim
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def election
|
|
33
|
-
@election ||= current_user&.admin? ? available_elections.
|
|
33
|
+
@election ||= current_user&.admin? ? available_elections.find_by(id: params[:id]) : elections.find_by(id: params[:id])
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def questions
|
|
@@ -59,6 +59,16 @@ module Decidim
|
|
|
59
59
|
with_any_state: "all"
|
|
60
60
|
}
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
def add_breadcrumb_item
|
|
64
|
+
return {} if election.blank?
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
label: translated_attribute(election.title),
|
|
68
|
+
url: Decidim::EngineRouter.main_proxy(current_component).election_path(election),
|
|
69
|
+
active: false
|
|
70
|
+
}
|
|
71
|
+
end
|
|
62
72
|
end
|
|
63
73
|
end
|
|
64
74
|
end
|
|
@@ -20,6 +20,14 @@ module Decidim
|
|
|
20
20
|
def update
|
|
21
21
|
enforce_permission_to(:create, :vote, election:)
|
|
22
22
|
|
|
23
|
+
response_ids = Array(params.dig(:response, question.id.to_s)).compact
|
|
24
|
+
|
|
25
|
+
if question.max_choices.present? && response_ids.size > question.max_choices
|
|
26
|
+
flash.now[:alert] = t("votes.question.max_choices_exceeded", scope: "decidim.elections", max: question.max_choices)
|
|
27
|
+
render :show
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
votes_buffer[question.id.to_s] = params.dig(:response, question.id.to_s)
|
|
24
32
|
redirect_to next_vote_step_path
|
|
25
33
|
end
|
|
@@ -10,6 +10,7 @@ module Decidim
|
|
|
10
10
|
|
|
11
11
|
attribute :question_type, String, default: "multiple_option"
|
|
12
12
|
attribute :response_options, Array[Decidim::Elections::Admin::ResponseOptionForm]
|
|
13
|
+
attribute :max_choices, Integer
|
|
13
14
|
attribute :deleted, Boolean, default: false
|
|
14
15
|
|
|
15
16
|
translatable_attribute :body, String
|
|
@@ -18,6 +19,7 @@ module Decidim
|
|
|
18
19
|
validates :body, translatable_presence: true
|
|
19
20
|
validates :question_type, inclusion: { in: Decidim::Elections::Question.question_types }, if: :editable?
|
|
20
21
|
validates :response_options, presence: true, if: :editable?
|
|
22
|
+
validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true
|
|
21
23
|
|
|
22
24
|
def election
|
|
23
25
|
@election ||= context[:election]
|
|
@@ -32,6 +34,10 @@ module Decidim
|
|
|
32
34
|
def editable?
|
|
33
35
|
@editable ||= id.blank? || Decidim::Elections::Question.exists?(id:)
|
|
34
36
|
end
|
|
37
|
+
|
|
38
|
+
def number_of_options
|
|
39
|
+
response_options.size
|
|
40
|
+
end
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -21,6 +21,7 @@ module Decidim
|
|
|
21
21
|
@filter_sections ||= begin
|
|
22
22
|
items = [{
|
|
23
23
|
method: :with_any_state,
|
|
24
|
+
name: "[with_any_state]",
|
|
24
25
|
collection: filter_elections_state_values,
|
|
25
26
|
label: t("decidim.elections.elections.filters.state"),
|
|
26
27
|
id: "date",
|
|
@@ -37,9 +38,13 @@ module Decidim
|
|
|
37
38
|
(defined?(current_component) && translated_attribute(current_component&.name).presence) || t("decidim.components.elections.name")
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def question_title(question, tag = :h3, **
|
|
41
|
-
content_tag(tag, **
|
|
42
|
-
translated_attribute(question.body)
|
|
41
|
+
def question_title(question, tag = :h3, **)
|
|
42
|
+
content_tag(tag, **) do
|
|
43
|
+
title = translated_attribute(question.body)
|
|
44
|
+
if question.max_choices.present? && question.question_type == "multiple_option"
|
|
45
|
+
title += " (#{t("decidim.elections.votes.question.max_choices", count: question.max_choices)})"
|
|
46
|
+
end
|
|
47
|
+
title
|
|
43
48
|
end
|
|
44
49
|
end
|
|
45
50
|
|
|
@@ -4,6 +4,7 @@ module Decidim
|
|
|
4
4
|
module Elections
|
|
5
5
|
class Vote < Elections::ApplicationRecord
|
|
6
6
|
include Decidim::Traceable
|
|
7
|
+
|
|
7
8
|
belongs_to :question, class_name: "Decidim::Elections::Question", counter_cache: true, inverse_of: :votes
|
|
8
9
|
belongs_to :response_option, class_name: "Decidim::Elections::ResponseOption", counter_cache: true, inverse_of: :votes
|
|
9
10
|
|
|
@@ -31,7 +32,7 @@ module Decidim
|
|
|
31
32
|
|
|
32
33
|
def max_votable_options
|
|
33
34
|
return unless question && response_option
|
|
34
|
-
return if question.votes.where.not(id:
|
|
35
|
+
return if question.votes.where.not(id:).where(voter_uid:).count < question.max_votable_options
|
|
35
36
|
|
|
36
37
|
errors.add(:response_option, :invalid)
|
|
37
38
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import "src/decidim/elections/elections.js"
|
|
1
2
|
import "src/decidim/elections/waiting_room.js"
|
|
2
3
|
import "src/decidim/elections/live_results_update.js";
|
|
4
|
+
import "src/decidim/elections/census_check_visibility.js";
|
|
3
5
|
import "src/decidim/elections/question_status_checker.js";
|
|
4
6
|
|
|
5
7
|
// Images
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
document.addEventListener("turbo:load", () => {
|
|
2
|
+
const checkbox = document.querySelector(".census-check-toggle");
|
|
3
|
+
|
|
4
|
+
if (!checkbox) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
checkbox.addEventListener("change", () => {
|
|
9
|
+
const url = checkbox.dataset.url;
|
|
10
|
+
const checked = checkbox.checked;
|
|
11
|
+
const csrfToken = document.querySelector("meta[name='csrf-token']");
|
|
12
|
+
|
|
13
|
+
if (!csrfToken) {
|
|
14
|
+
console.error("CSRF token not found. Please refresh the page.");
|
|
15
|
+
checkbox.checked = !checked;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fetch(url, {
|
|
20
|
+
method: "PATCH",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"X-CSRF-Token": csrfToken.content
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
// eslint-disable-next-line camelcase
|
|
27
|
+
allow_census_check_before_start: checked
|
|
28
|
+
})
|
|
29
|
+
}).then((response) => {
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
return response.json();
|
|
34
|
+
}).then((data) => {
|
|
35
|
+
if (!data.success) {
|
|
36
|
+
checkbox.checked = !checked;
|
|
37
|
+
console.error(`Error updating setting: ${data.error || "Unknown error"}`);
|
|
38
|
+
}
|
|
39
|
+
}).catch((error) => {
|
|
40
|
+
checkbox.checked = !checked;
|
|
41
|
+
console.error(`Error updating setting: ${error.message}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2
|
+
const censusCheckButton = document.querySelector("[data-census-check-button]");
|
|
3
|
+
|
|
4
|
+
if (!censusCheckButton) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const url = censusCheckButton.dataset.censusCheckUrl;
|
|
9
|
+
|
|
10
|
+
if (!url) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const explanation = document.querySelector("[data-census-check-explanation]");
|
|
15
|
+
|
|
16
|
+
const toggleVisibility = (show) => {
|
|
17
|
+
censusCheckButton.classList.toggle("hidden", !show);
|
|
18
|
+
if (explanation) {
|
|
19
|
+
explanation.classList.toggle("hidden", !show);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const updateVisibility = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: {
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const shouldShow = data.allow_census_check_before_start && data.census_ready && data.scheduled;
|
|
40
|
+
|
|
41
|
+
toggleVisibility(shouldShow);
|
|
42
|
+
|
|
43
|
+
if (data.scheduled) {
|
|
44
|
+
setTimeout(updateVisibility, 4000);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
setTimeout(updateVisibility, 4000);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
updateVisibility();
|
|
52
|
+
});
|