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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/decidim_consultations_manifest.css +0 -1
  3. data/app/assets/config/decidim_consultations_manifest.js +1 -1
  4. data/app/assets/javascripts/decidim/consultations/utils_multiple.js +21 -0
  5. data/app/assets/javascripts/decidim/consultations/vote_dialog.js +13 -0
  6. data/app/cells/decidim/consultations/consultation_m/data.erb +2 -2
  7. data/app/cells/decidim/consultations/content_blocks/highlighted_consultations_cell.rb +1 -0
  8. data/app/commands/decidim/consultations/admin/create_consultation.rb +2 -0
  9. data/app/commands/decidim/consultations/admin/create_question.rb +2 -0
  10. data/app/commands/decidim/consultations/admin/create_response.rb +1 -0
  11. data/app/commands/decidim/consultations/admin/update_consultation.rb +1 -0
  12. data/app/commands/decidim/consultations/admin/update_question.rb +1 -0
  13. data/app/commands/decidim/consultations/admin/update_question_configuration.rb +55 -0
  14. data/app/commands/decidim/consultations/admin/update_response.rb +1 -0
  15. data/app/commands/decidim/consultations/multiple_vote_question.rb +47 -0
  16. data/app/controllers/concerns/decidim/consultations/admin/question_admin.rb +1 -0
  17. data/app/controllers/concerns/decidim/consultations/needs_question.rb +24 -2
  18. data/app/controllers/concerns/decidim/consultations/orderable.rb +1 -21
  19. data/app/controllers/decidim/consultations/admin/consultations_controller.rb +7 -1
  20. data/app/controllers/decidim/consultations/admin/question_configuration_controller.rb +54 -0
  21. data/app/controllers/decidim/consultations/admin/question_permissions_controller.rb +36 -0
  22. data/app/controllers/decidim/consultations/admin/questions_controller.rb +1 -0
  23. data/app/controllers/decidim/consultations/authorization_vote_modals_controller.rb +26 -0
  24. data/app/controllers/decidim/consultations/consultations_controller.rb +1 -1
  25. data/app/controllers/decidim/consultations/question_multiple_votes_controller.rb +39 -0
  26. data/app/controllers/decidim/consultations/question_votes_controller.rb +1 -1
  27. data/app/forms/decidim/consultations/admin/question_configuration_form.rb +27 -0
  28. data/app/forms/decidim/consultations/multi_vote_form.rb +41 -0
  29. data/app/forms/decidim/consultations/vote_form.rb +1 -0
  30. data/app/helpers/decidim/consultations/questions_helper.rb +25 -4
  31. data/app/models/decidim/consultation.rb +9 -7
  32. data/app/models/decidim/consultations/question.rb +26 -7
  33. data/app/models/decidim/consultations/vote.rb +11 -1
  34. data/app/permissions/decidim/consultations/admin/permissions.rb +1 -1
  35. data/app/permissions/decidim/consultations/permissions.rb +10 -0
  36. data/app/views/decidim/consultations/admin/consultations/edit.html.erb +4 -1
  37. data/app/views/decidim/consultations/admin/consultations/index.html.erb +2 -2
  38. data/app/views/decidim/consultations/admin/consultations/results.html.erb +42 -0
  39. data/app/views/decidim/consultations/admin/question_configuration/_form.html.erb +19 -0
  40. data/app/views/decidim/consultations/admin/question_configuration/edit.html.erb +12 -0
  41. data/app/views/decidim/consultations/admin/questions/edit.html.erb +1 -0
  42. data/app/views/decidim/consultations/admin/questions/index.html.erb +6 -2
  43. data/app/views/decidim/consultations/consultations/_consultations.html.erb +1 -1
  44. data/app/views/decidim/consultations/consultations/_highlighted_questions.html.erb +7 -5
  45. data/app/views/decidim/consultations/question_multiple_votes/_form.html.erb +14 -0
  46. data/app/views/decidim/consultations/question_multiple_votes/_results_rules.html.erb +14 -0
  47. data/app/views/decidim/consultations/question_multiple_votes/_voting_rules.html.erb +29 -0
  48. data/app/views/decidim/consultations/question_multiple_votes/show.html.erb +36 -0
  49. data/app/views/decidim/consultations/question_votes/update_vote_button.js.erb +9 -0
  50. data/app/views/decidim/consultations/questions/_results.html.erb +4 -0
  51. data/app/views/decidim/consultations/questions/_vote_button.html.erb +29 -4
  52. data/app/views/decidim/consultations/questions/_vote_modal_confirm.html.erb +1 -0
  53. data/app/views/decidim/consultations/questions/show.html.erb +1 -1
  54. data/app/views/layouts/decidim/_question_header.html.erb +2 -14
  55. data/app/views/layouts/decidim/_question_header_buttons.html.erb +11 -0
  56. data/app/views/layouts/decidim/admin/consultation.html.erb +4 -0
  57. data/app/views/layouts/decidim/admin/question.html.erb +66 -44
  58. data/app/views/layouts/decidim/question.html.erb +3 -3
  59. data/app/views/layouts/decidim/question_multivote.html.erb +9 -0
  60. data/config/locales/ar.yml +8 -0
  61. data/config/locales/ca.yml +51 -0
  62. data/config/locales/cs.yml +51 -0
  63. data/config/locales/en.yml +51 -0
  64. data/config/locales/es-MX.yml +13 -0
  65. data/config/locales/es-PY.yml +13 -0
  66. data/config/locales/es.yml +51 -0
  67. data/config/locales/fi-plain.yml +51 -0
  68. data/config/locales/fi.yml +51 -0
  69. data/config/locales/fr.yml +50 -0
  70. data/config/locales/hu.yml +51 -0
  71. data/config/locales/it.yml +15 -0
  72. data/config/locales/nl.yml +52 -1
  73. data/config/locales/sv.yml +9 -0
  74. data/config/locales/tr-TR.yml +5 -0
  75. data/db/migrate/20190702162755_add_options_to_decidim_consultations_questions.rb +8 -0
  76. data/db/migrate/20190710121122_add_free_instructions_field_to_consultations_questions.rb +7 -0
  77. data/lib/decidim/consultations/admin_engine.rb +3 -0
  78. data/lib/decidim/consultations/engine.rb +4 -0
  79. data/lib/decidim/consultations/participatory_space.rb +5 -0
  80. data/lib/decidim/consultations/test/factories.rb +5 -0
  81. data/lib/decidim/consultations/version.rb +1 -1
  82. metadata +29 -11
  83. data/app/assets/javascripts/decidim/consultations/social_share.js +0 -2
  84. 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
@@ -5,6 +5,7 @@ module Decidim
5
5
  module Admin
6
6
  class QuestionsController < Decidim::Consultations::Admin::ApplicationController
7
7
  include QuestionAdmin
8
+ helper ::Decidim::Admin::ResourcePermissionsHelper
8
9
 
9
10
  def index
10
11
  enforce_permission_to :read, :question
@@ -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
@@ -23,7 +23,7 @@ module Decidim
23
23
  on(:invalid) do
24
24
  render json: {
25
25
  error: I18n.t("question_votes.create.error", scope: "decidim.consultations")
26
- }, status: 422
26
+ }, status: :unprocessable_entity
27
27
  end
28
28
  end
29
29
  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
@@ -18,6 +18,7 @@ module Decidim
18
18
 
19
19
  def response_exists
20
20
  return unless response.nil?
21
+
21
22
  errors.add(
22
23
  :decidim_consultations_response_id,
23
24
  I18n.t("decidim_consultations_response_id.not_found", scope: "activemodel.errors.vote")
@@ -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 = previous_question || current_question
16
- css << " disabled" if previous_question.nil?
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 = next_question || current_question
20
- css << " disabled" if next_question.nil?
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(@form, html: { class: "form edit_consultation" }) do |f| %>
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" %>