decidim-consultations 0.18.1 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/config/decidim_consultations_manifest.css +0 -1
- data/app/assets/config/decidim_consultations_manifest.js +1 -1
- data/app/assets/javascripts/decidim/consultations/utils_multiple.js +21 -0
- data/app/assets/javascripts/decidim/consultations/vote_dialog.js +13 -0
- data/app/cells/decidim/consultations/consultation_m/data.erb +2 -2
- data/app/cells/decidim/consultations/content_blocks/highlighted_consultations_cell.rb +1 -0
- data/app/commands/decidim/consultations/admin/create_consultation.rb +2 -0
- data/app/commands/decidim/consultations/admin/create_question.rb +2 -0
- data/app/commands/decidim/consultations/admin/create_response.rb +1 -0
- data/app/commands/decidim/consultations/admin/update_consultation.rb +1 -0
- data/app/commands/decidim/consultations/admin/update_question.rb +1 -0
- data/app/commands/decidim/consultations/admin/update_question_configuration.rb +55 -0
- data/app/commands/decidim/consultations/admin/update_response.rb +1 -0
- data/app/commands/decidim/consultations/multiple_vote_question.rb +47 -0
- data/app/controllers/concerns/decidim/consultations/admin/question_admin.rb +1 -0
- data/app/controllers/concerns/decidim/consultations/needs_question.rb +24 -2
- data/app/controllers/concerns/decidim/consultations/orderable.rb +1 -21
- data/app/controllers/decidim/consultations/admin/consultations_controller.rb +7 -1
- data/app/controllers/decidim/consultations/admin/question_configuration_controller.rb +54 -0
- data/app/controllers/decidim/consultations/admin/question_permissions_controller.rb +36 -0
- data/app/controllers/decidim/consultations/admin/questions_controller.rb +1 -0
- data/app/controllers/decidim/consultations/authorization_vote_modals_controller.rb +26 -0
- data/app/controllers/decidim/consultations/consultations_controller.rb +1 -1
- data/app/controllers/decidim/consultations/question_multiple_votes_controller.rb +39 -0
- data/app/controllers/decidim/consultations/question_votes_controller.rb +1 -1
- data/app/forms/decidim/consultations/admin/question_configuration_form.rb +27 -0
- data/app/forms/decidim/consultations/multi_vote_form.rb +41 -0
- data/app/forms/decidim/consultations/vote_form.rb +1 -0
- data/app/helpers/decidim/consultations/questions_helper.rb +25 -4
- data/app/models/decidim/consultation.rb +9 -7
- data/app/models/decidim/consultations/question.rb +26 -7
- data/app/models/decidim/consultations/vote.rb +11 -1
- data/app/permissions/decidim/consultations/admin/permissions.rb +1 -1
- data/app/permissions/decidim/consultations/permissions.rb +10 -0
- data/app/views/decidim/consultations/admin/consultations/edit.html.erb +4 -1
- data/app/views/decidim/consultations/admin/consultations/index.html.erb +2 -2
- data/app/views/decidim/consultations/admin/consultations/results.html.erb +42 -0
- data/app/views/decidim/consultations/admin/question_configuration/_form.html.erb +19 -0
- data/app/views/decidim/consultations/admin/question_configuration/edit.html.erb +12 -0
- data/app/views/decidim/consultations/admin/questions/edit.html.erb +1 -0
- data/app/views/decidim/consultations/admin/questions/index.html.erb +6 -2
- data/app/views/decidim/consultations/consultations/_consultations.html.erb +1 -1
- data/app/views/decidim/consultations/consultations/_highlighted_questions.html.erb +7 -5
- data/app/views/decidim/consultations/question_multiple_votes/_form.html.erb +14 -0
- data/app/views/decidim/consultations/question_multiple_votes/_results_rules.html.erb +14 -0
- data/app/views/decidim/consultations/question_multiple_votes/_voting_rules.html.erb +29 -0
- data/app/views/decidim/consultations/question_multiple_votes/show.html.erb +36 -0
- data/app/views/decidim/consultations/question_votes/update_vote_button.js.erb +9 -0
- data/app/views/decidim/consultations/questions/_results.html.erb +4 -0
- data/app/views/decidim/consultations/questions/_vote_button.html.erb +29 -4
- data/app/views/decidim/consultations/questions/_vote_modal_confirm.html.erb +1 -0
- data/app/views/decidim/consultations/questions/show.html.erb +1 -1
- data/app/views/layouts/decidim/_question_header.html.erb +2 -14
- data/app/views/layouts/decidim/_question_header_buttons.html.erb +11 -0
- data/app/views/layouts/decidim/admin/consultation.html.erb +4 -0
- data/app/views/layouts/decidim/admin/question.html.erb +66 -44
- data/app/views/layouts/decidim/question.html.erb +3 -3
- data/app/views/layouts/decidim/question_multivote.html.erb +9 -0
- data/config/locales/ar.yml +8 -0
- data/config/locales/ca.yml +51 -0
- data/config/locales/cs.yml +51 -0
- data/config/locales/en.yml +51 -0
- data/config/locales/es-MX.yml +13 -0
- data/config/locales/es-PY.yml +13 -0
- data/config/locales/es.yml +51 -0
- data/config/locales/fi-plain.yml +51 -0
- data/config/locales/fi.yml +51 -0
- data/config/locales/fr.yml +50 -0
- data/config/locales/hu.yml +51 -0
- data/config/locales/it.yml +15 -0
- data/config/locales/nl.yml +52 -1
- data/config/locales/sv.yml +9 -0
- data/config/locales/tr-TR.yml +5 -0
- data/db/migrate/20190702162755_add_options_to_decidim_consultations_questions.rb +8 -0
- data/db/migrate/20190710121122_add_free_instructions_field_to_consultations_questions.rb +7 -0
- data/lib/decidim/consultations/admin_engine.rb +3 -0
- data/lib/decidim/consultations/engine.rb +4 -0
- data/lib/decidim/consultations/participatory_space.rb +5 -0
- data/lib/decidim/consultations/test/factories.rb +5 -0
- data/lib/decidim/consultations/version.rb +1 -1
- metadata +29 -11
- data/app/assets/javascripts/decidim/consultations/social_share.js +0 -2
- data/app/assets/stylesheets/decidim/consultations/social_share.css.scss +0 -18
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Consultations
|
5
|
+
module Admin
|
6
|
+
# Controller that allows managing Questions
|
7
|
+
# permissions in the admin panel.
|
8
|
+
class QuestionPermissionsController < Decidim::Admin::ResourcePermissionsController
|
9
|
+
include QuestionAdmin
|
10
|
+
# layout "decidim/admin/Question"
|
11
|
+
|
12
|
+
register_permissions(::Decidim::Consultations::Admin::QuestionPermissionsController,
|
13
|
+
::Decidim::Consultations::Permissions,
|
14
|
+
::Decidim::Admin::Permissions)
|
15
|
+
|
16
|
+
def permission_class_chain
|
17
|
+
::Decidim.permissions_registry.chain_for(::Decidim::Consultations::Admin::QuestionPermissionsController)
|
18
|
+
end
|
19
|
+
|
20
|
+
def edit
|
21
|
+
enforce_permission_to :update, :question, question: current_question
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def update
|
26
|
+
enforce_permission_to :update, :question, question: current_question
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def return_path
|
31
|
+
consultation_questions_path current_consultation
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Consultations
|
5
|
+
class AuthorizationVoteModalsController < Decidim::Consultations::ApplicationController
|
6
|
+
include NeedsQuestion
|
7
|
+
|
8
|
+
helper_method :authorizations, :authorize_action_path
|
9
|
+
layout false
|
10
|
+
|
11
|
+
def show
|
12
|
+
render template: "decidim/authorization_modals/show"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def authorize_action_path(handler_name)
|
18
|
+
authorizations.status_for(handler_name).current_path(redirect_url: URI(request.referer).path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorizations
|
22
|
+
@authorizations ||= action_authorized_to(:vote, resource: nil, permissions_holder: current_question)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -10,7 +10,7 @@ module Decidim
|
|
10
10
|
include NeedsConsultation
|
11
11
|
include FilterResource
|
12
12
|
include Paginable
|
13
|
-
include Orderable
|
13
|
+
include Decidim::Consultations::Orderable
|
14
14
|
include ParticipatorySpaceContext
|
15
15
|
|
16
16
|
helper_method :collection, :consultations, :finished_consultations, :active_consultations, :filter
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Consultations
|
5
|
+
class QuestionMultipleVotesController < Decidim::Consultations::ApplicationController
|
6
|
+
layout "layouts/decidim/question_multivote"
|
7
|
+
include NeedsQuestion
|
8
|
+
include Decidim::FormFactory
|
9
|
+
|
10
|
+
helper QuestionsHelper
|
11
|
+
|
12
|
+
before_action :authenticate_user!
|
13
|
+
|
14
|
+
# Non-ajax votings (such as multi-reponses) have a html page
|
15
|
+
def show
|
16
|
+
enforce_permission_to :vote, :question, question: current_question
|
17
|
+
@form = form(MultiVoteForm).instance(current_question: current_question)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create
|
21
|
+
enforce_permission_to :vote, :question, question: current_question
|
22
|
+
|
23
|
+
multivote_form = form(MultiVoteForm).from_params(params, current_question: current_question)
|
24
|
+
|
25
|
+
MultipleVoteQuestion.call(multivote_form, current_user) do
|
26
|
+
on(:ok) do
|
27
|
+
redirect_to question_path(current_question)
|
28
|
+
end
|
29
|
+
|
30
|
+
on(:invalid) do |_form, error|
|
31
|
+
flash[:error] = I18n.t("question_votes.create.error", scope: "decidim.consultations")
|
32
|
+
flash[:error] << " (#{error})" if error
|
33
|
+
redirect_to question_question_multiple_votes_path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Consultations
|
5
|
+
module Admin
|
6
|
+
# A form object used to create questions for a consultation from the admin dashboard.
|
7
|
+
class QuestionConfigurationForm < Form
|
8
|
+
include TranslatableAttributes
|
9
|
+
mimic :question
|
10
|
+
|
11
|
+
attribute :max_votes, Integer, default: 1
|
12
|
+
attribute :min_votes, Integer, default: 1
|
13
|
+
translatable_attribute :instructions, String
|
14
|
+
|
15
|
+
validates :max_votes, numericality: { greater_than_or_equal_to: 1 }
|
16
|
+
validates :min_votes, numericality: { greater_than_or_equal_to: 1 }
|
17
|
+
validate :min_lower_than_max
|
18
|
+
|
19
|
+
def min_lower_than_max
|
20
|
+
return if min_votes.to_i <= max_votes.to_i
|
21
|
+
|
22
|
+
errors.add(:max_votes, :lower_than_min)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module Consultations
|
5
|
+
# This form validates a MultiVote Question
|
6
|
+
class MultiVoteForm < Form
|
7
|
+
mimic :responses
|
8
|
+
|
9
|
+
attribute :responses, Array[Integer]
|
10
|
+
|
11
|
+
validate :valid_num_of_votes
|
12
|
+
validate :valid_responses
|
13
|
+
|
14
|
+
def vote_forms
|
15
|
+
@vote_forms ||= responses.map do |response_id|
|
16
|
+
VoteForm.from_params(decidim_consultations_response_id: response_id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def valid_num_of_votes
|
23
|
+
return if responses.count.between?(context.current_question.min_votes, context.current_question.max_votes)
|
24
|
+
|
25
|
+
errors.add(
|
26
|
+
:responses,
|
27
|
+
I18n.t("activerecord.errors.models.decidim/consultations/vote.attributes.question.invalid_num_votes")
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_responses
|
32
|
+
return if vote_forms.all?(&:valid?)
|
33
|
+
|
34
|
+
errors.add(
|
35
|
+
:responses,
|
36
|
+
I18n.t("decidim_consultations_response_id.not_found", scope: "activemodel.errors.vote")
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -9,19 +9,40 @@ module Decidim
|
|
9
9
|
def display_next_previous_button(direction, optional_classes = "")
|
10
10
|
css = "card__button button hollow " + optional_classes
|
11
11
|
|
12
|
+
# Do not show anything if is a lonely question
|
13
|
+
return unless previous_published_question || next_published_question
|
14
|
+
|
12
15
|
case direction
|
13
16
|
when :previous
|
14
17
|
i18n_text = t("previous_button", scope: "decidim.questions")
|
15
|
-
question =
|
16
|
-
css << " disabled" if
|
18
|
+
question = previous_published_question || current_question
|
19
|
+
css << " disabled" if previous_published_question.nil?
|
17
20
|
when :next
|
18
21
|
i18n_text = t("next_button", scope: "decidim.questions")
|
19
|
-
question =
|
20
|
-
css << " disabled" if
|
22
|
+
question = next_published_question || current_question
|
23
|
+
css << " disabled" if next_published_question.nil?
|
21
24
|
end
|
22
25
|
|
23
26
|
link_to(i18n_text, decidim_consultations.question_path(question), class: css)
|
24
27
|
end
|
28
|
+
|
29
|
+
def authorized_vote_modal_button(question, html_options, &block)
|
30
|
+
return if current_user && action_authorized_to(:vote, resource: nil, permissions_holder: question).ok?
|
31
|
+
|
32
|
+
tag = "button"
|
33
|
+
html_options ||= {}
|
34
|
+
|
35
|
+
if !current_user
|
36
|
+
html_options["data-open"] = "loginModal"
|
37
|
+
else
|
38
|
+
html_options["data-open"] = "authorizationModal"
|
39
|
+
html_options["data-open-url"] = decidim_consultations.authorization_vote_modal_question_path(question)
|
40
|
+
end
|
41
|
+
|
42
|
+
html_options["onclick"] = "event.preventDefault();"
|
43
|
+
|
44
|
+
send("#{tag}_to", "", html_options, &block)
|
45
|
+
end
|
25
46
|
end
|
26
47
|
end
|
27
48
|
end
|
@@ -10,6 +10,7 @@ module Decidim
|
|
10
10
|
include Decidim::Traceable
|
11
11
|
include Decidim::Loggable
|
12
12
|
include Decidim::ParticipatorySpaceResourceable
|
13
|
+
include Decidim::Randomable
|
13
14
|
|
14
15
|
belongs_to :organization,
|
15
16
|
foreign_key: "decidim_organization_id",
|
@@ -64,6 +65,14 @@ module Decidim
|
|
64
65
|
questions.published.group_by(&:scope)
|
65
66
|
end
|
66
67
|
|
68
|
+
def total_votes
|
69
|
+
@total_votes ||= questions.published.sum(:votes_count)
|
70
|
+
end
|
71
|
+
|
72
|
+
def total_participants
|
73
|
+
@total_participants ||= questions.published.joins(:votes).select(:decidim_author_id).distinct.count
|
74
|
+
end
|
75
|
+
|
67
76
|
# This method exists with the only purpose of getting rid of whats seems to be an issue in
|
68
77
|
# the new scope picker: This engine is a bit special: consultations and questions are a kind of
|
69
78
|
# nested participatory spaces. When a new question is created the consultation is the participatory space.
|
@@ -73,13 +82,6 @@ module Decidim
|
|
73
82
|
nil
|
74
83
|
end
|
75
84
|
|
76
|
-
def self.order_randomly(seed)
|
77
|
-
transaction do
|
78
|
-
connection.execute("SELECT setseed(#{connection.quote(seed)})")
|
79
|
-
select('"decidim_consultations".*, RANDOM()').order(Arel.sql("RANDOM()")).load
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
85
|
def closed?
|
84
86
|
!active?
|
85
87
|
end
|
@@ -4,6 +4,7 @@ module Decidim
|
|
4
4
|
module Consultations
|
5
5
|
# The data store for Consultation questions in the Decidim::Consultations component.
|
6
6
|
class Question < ApplicationRecord
|
7
|
+
include Decidim::HasResourcePermission
|
7
8
|
include Decidim::Participable
|
8
9
|
include Decidim::Publicable
|
9
10
|
include Decidim::Scopable
|
@@ -14,6 +15,7 @@ module Decidim
|
|
14
15
|
include Decidim::Traceable
|
15
16
|
include Decidim::Loggable
|
16
17
|
include Decidim::ParticipatorySpaceResourceable
|
18
|
+
include Decidim::Randomable
|
17
19
|
|
18
20
|
belongs_to :consultation,
|
19
21
|
foreign_key: "decidim_consultation_id",
|
@@ -59,14 +61,33 @@ module Decidim
|
|
59
61
|
responses.order(votes_count: :desc)
|
60
62
|
end
|
61
63
|
|
64
|
+
# if results can be shown to admins
|
65
|
+
def publishable_results?
|
66
|
+
consultation.finished? && sorted_results.any?
|
67
|
+
end
|
68
|
+
|
62
69
|
def most_voted_response
|
63
70
|
@most_voted_response ||= responses.order(votes_count: :desc).first
|
64
71
|
end
|
65
72
|
|
73
|
+
# Total number of votes, on multiple votes questions does not match users voting
|
66
74
|
def total_votes
|
67
75
|
@total_votes ||= responses.sum(&:votes_count)
|
68
76
|
end
|
69
77
|
|
78
|
+
# Total number of users voting
|
79
|
+
def total_participants
|
80
|
+
@total_participants ||= votes.select(:decidim_author_id).distinct.count
|
81
|
+
end
|
82
|
+
|
83
|
+
# Multiple answers allowed?
|
84
|
+
def multiple?
|
85
|
+
return false if external_voting
|
86
|
+
return false if max_votes.blank?
|
87
|
+
|
88
|
+
max_votes > 1
|
89
|
+
end
|
90
|
+
|
70
91
|
# Public: Overrides the `comments_have_alignment?` Commentable concern method.
|
71
92
|
def comments_have_alignment?
|
72
93
|
true
|
@@ -142,16 +163,14 @@ module Decidim
|
|
142
163
|
Decidim.find_participatory_space_manifest(Decidim::Consultation.name.demodulize.underscore.pluralize)
|
143
164
|
end
|
144
165
|
|
145
|
-
def self.order_randomly(seed)
|
146
|
-
transaction do
|
147
|
-
connection.execute("SELECT setseed(#{connection.quote(seed)})")
|
148
|
-
select('"decidim_consultations_questions".*, RANDOM()').order(Arel.sql("RANDOM()")).load
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
166
|
def resource_description
|
153
167
|
subtitle
|
154
168
|
end
|
169
|
+
|
170
|
+
# Public: Overrides the `allow_resource_permissions?` Resourceable concern method.
|
171
|
+
def allow_resource_permissions?
|
172
|
+
true
|
173
|
+
end
|
155
174
|
end
|
156
175
|
end
|
157
176
|
end
|
@@ -18,7 +18,8 @@ module Decidim
|
|
18
18
|
inverse_of: :votes,
|
19
19
|
counter_cache: :votes_count
|
20
20
|
|
21
|
-
validates :author, uniqueness: { scope: [:decidim_user_group_id, :question] }
|
21
|
+
validates :author, uniqueness: { scope: [:decidim_user_group_id, :question] }, unless: -> { question.multiple? }
|
22
|
+
validate :votes_per_author
|
22
23
|
validate :author_and_question_same_organization
|
23
24
|
|
24
25
|
delegate :organization, to: :question
|
@@ -28,8 +29,17 @@ module Decidim
|
|
28
29
|
# Private: check if the question and the author have the same organization
|
29
30
|
def author_and_question_same_organization
|
30
31
|
return if !question || !author
|
32
|
+
|
31
33
|
errors.add(:question, :invalid) unless author.organization == question.organization
|
32
34
|
end
|
35
|
+
|
36
|
+
# Author can vote multiple times constrained to question limits
|
37
|
+
def votes_per_author
|
38
|
+
return unless question.multiple?
|
39
|
+
|
40
|
+
total = Vote.where(author: author, question: question, decidim_user_group_id: decidim_user_group_id).count
|
41
|
+
errors.add(:question, :invalid_num_votes) unless total < question.max_votes
|
42
|
+
end
|
33
43
|
end
|
34
44
|
end
|
35
45
|
end
|
@@ -72,7 +72,7 @@ module Decidim
|
|
72
72
|
case permission_action.action
|
73
73
|
when :create, :read
|
74
74
|
allow!
|
75
|
-
when :update, :destroy, :preview
|
75
|
+
when :update, :destroy, :preview, :configure
|
76
76
|
toggle_allow(question.present?)
|
77
77
|
when :publish
|
78
78
|
toggle_allow(question.external_voting || question.responses_count.positive?)
|
@@ -7,6 +7,7 @@ module Decidim
|
|
7
7
|
allowed_public_anonymous_action?
|
8
8
|
|
9
9
|
return permission_action unless user
|
10
|
+
|
10
11
|
allowed_public_action?
|
11
12
|
|
12
13
|
return Decidim::Consultations::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin
|
@@ -24,6 +25,12 @@ module Decidim
|
|
24
25
|
@consultation ||= context.fetch(:consultation, nil)
|
25
26
|
end
|
26
27
|
|
28
|
+
def authorized?(permission_action, resource: nil)
|
29
|
+
return unless resource || question
|
30
|
+
|
31
|
+
ActionAuthorizer.new(user, permission_action, question, resource).authorize.ok?
|
32
|
+
end
|
33
|
+
|
27
34
|
def allowed_public_anonymous_action?
|
28
35
|
return unless permission_action.action == :read
|
29
36
|
return unless permission_action.scope == :public
|
@@ -42,6 +49,9 @@ module Decidim
|
|
42
49
|
return unless permission_action.scope == :public
|
43
50
|
return unless permission_action.subject == :question
|
44
51
|
|
52
|
+
# check if question has been limited by admins first
|
53
|
+
return unless authorized? :vote
|
54
|
+
|
45
55
|
case permission_action.action
|
46
56
|
when :vote
|
47
57
|
toggle_allow(question.can_be_voted_by?(user))
|
@@ -1,4 +1,7 @@
|
|
1
|
-
<%= decidim_form_for
|
1
|
+
<%= decidim_form_for @form,
|
2
|
+
method: :patch,
|
3
|
+
url: consultation_path(current_consultation),
|
4
|
+
html: { class: "form edit_consultation" } do |f| %>
|
2
5
|
<%= render partial: "form", object: f %>
|
3
6
|
<div class="button--double form-general-submit">
|
4
7
|
<%= f.submit t("consultations.edit.update", scope: "decidim.admin"), class: "button" %>
|