decidim-proposals 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +2 -0
  3. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +2 -0
  4. data/app/controllers/decidim/proposals/proposals_controller.rb +42 -5
  5. data/app/forms/decidim/proposals/proposal_form.rb +3 -1
  6. data/app/helpers/decidim/proposals/proposal_votes_helper.rb +1 -1
  7. data/app/models/decidim/proposals/abilities/admin_user.rb +45 -0
  8. data/app/models/decidim/proposals/abilities/current_user.rb +22 -4
  9. data/app/models/decidim/proposals/abilities/process_admin_user.rb +57 -0
  10. data/app/models/decidim/proposals/proposal.rb +23 -2
  11. data/app/services/decidim/proposals/proposal_search.rb +25 -13
  12. data/app/views/decidim/proposals/admin/proposal_answers/edit.html.erb +1 -1
  13. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +1 -1
  14. data/app/views/decidim/proposals/admin/proposals/index.html.erb +16 -10
  15. data/app/views/decidim/proposals/admin/proposals/new.html.erb +1 -1
  16. data/app/views/decidim/proposals/proposal_votes/update_buttons_and_counters.js.erb +4 -4
  17. data/app/views/decidim/proposals/proposals/_filters.html.erb +15 -3
  18. data/app/views/decidim/proposals/proposals/_proposal.html.erb +3 -11
  19. data/app/views/decidim/proposals/proposals/_proposals.html.erb +2 -1
  20. data/app/views/decidim/proposals/proposals/_remaining_votes_count.html.erb +3 -1
  21. data/app/views/decidim/proposals/proposals/_tags.html.erb +10 -0
  22. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +23 -21
  23. data/app/views/decidim/proposals/proposals/_votes_count.html.erb +9 -6
  24. data/app/views/decidim/proposals/proposals/_votes_limit.html.erb +1 -3
  25. data/app/views/decidim/proposals/proposals/index.html.erb +3 -3
  26. data/app/views/decidim/proposals/proposals/index.js.erb +6 -1
  27. data/app/views/decidim/proposals/proposals/new.html.erb +2 -2
  28. data/app/views/decidim/proposals/proposals/show.html.erb +23 -28
  29. data/config/i18n-tasks.yml +2 -0
  30. data/config/locales/ca.yml +30 -15
  31. data/config/locales/en.yml +19 -4
  32. data/config/locales/es.yml +19 -4
  33. data/config/locales/eu.yml +5 -0
  34. data/db/migrate/20170205082832_add_index_to_decidim_proposals_proposals_proposal_votes_count.rb +7 -0
  35. data/lib/decidim/proposals/admin_engine.rb +7 -0
  36. data/lib/decidim/proposals/feature.rb +16 -3
  37. data/lib/decidim/proposals/test/factories.rb +1 -1
  38. metadata +19 -15
  39. data/app/views/decidim/proposals/proposals/_share.html.erb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72d9615abcde37945383d87bb3f7c74126d66b3c
4
- data.tar.gz: 588e35fae074a55307aab6789d6d3af25db87d9a
3
+ metadata.gz: 1fa61b404c05485715f4617683618718c79f9a92
4
+ data.tar.gz: 3afb55cb4acae13ff3f0a63cd74432eb67c8cc6f
5
5
  SHA512:
6
- metadata.gz: a883d5c8a999da8fb3f0bda1811678905858d1693f69b846b975b66f2c25cbdbf0bfa7c671ceb991519f2b68972ae2cef71bd08be7a21f03cda07a5dfbe6cc9e
7
- data.tar.gz: d0882dff25bb3c47491036de7988d4e1fd392e6d1806e49a287ebcd4cf02e9e70a6a64b829e07102e16607ea31363ba17afb1dbed0968450df66e02ba2033330
6
+ metadata.gz: f9927865a15b7e1310566776820ffd86fa29788fa6db29dc0c030afd681f7055f1991e8c031a043ee80d2219698440c25d2d80ff796b2a112319cd551f7f0445
7
+ data.tar.gz: 9e56593a4e2fc70245eb0c91d60e3172938c3b9932725ef94e385e8ce2dae2522f49d24ac950b38b25e0b1e920f8036bb09fa9dfd09272c08b54dc573d93710b
@@ -7,10 +7,12 @@ module Decidim
7
7
  helper_method :proposal
8
8
 
9
9
  def edit
10
+ authorize! :update, proposal
10
11
  @form = form(Admin::ProposalAnswerForm).from_model(proposal)
11
12
  end
12
13
 
13
14
  def update
15
+ authorize! :update, proposal
14
16
  @form = form(Admin::ProposalAnswerForm).from_params(params)
15
17
 
16
18
  Admin::AnswerProposal.call(@form, proposal) do
@@ -8,10 +8,12 @@ module Decidim
8
8
  helper_method :proposals
9
9
 
10
10
  def new
11
+ authorize! :create, Proposal
11
12
  @form = form(Admin::ProposalForm).from_params({})
12
13
  end
13
14
 
14
15
  def create
16
+ authorize! :create, Proposal
15
17
  @form = form(Admin::ProposalForm).from_params(params)
16
18
 
17
19
  Admin::CreateProposal.call(@form) do
@@ -7,6 +7,8 @@ module Decidim
7
7
  include FormFactory
8
8
  include FilterResource
9
9
 
10
+ helper_method :order, :random_seed
11
+
10
12
  before_action :authenticate_user!, only: [:new, :create]
11
13
 
12
14
  def show
@@ -17,11 +19,20 @@ module Decidim
17
19
  @proposals = search
18
20
  .results
19
21
  .includes(:author)
20
- .includes(votes: [:author])
21
- .page(params[:page])
22
- .per(12)
22
+ .includes(:category)
23
+ .includes(:scope)
24
+
25
+ @proposals = @proposals.page(params[:page]).per(12)
26
+ @proposals = reorder(@proposals)
23
27
 
24
- @random_seed = search.random_seed
28
+ @voted_proposals = if current_user
29
+ ProposalVote.where(
30
+ author: current_user,
31
+ proposal: @proposals
32
+ ).pluck(:decidim_proposal_id)
33
+ else
34
+ []
35
+ end
25
36
  end
26
37
 
27
38
  def new
@@ -50,6 +61,31 @@ module Decidim
50
61
 
51
62
  private
52
63
 
64
+ def order
65
+ @order = params[:order] || "random"
66
+ end
67
+
68
+ # Returns: A random float number between -1 and 1 to be used as a random seed at the database.
69
+ def random_seed
70
+ @random_seed ||= (params[:random_seed] ? params[:random_seed].to_f : (rand * 2 - 1))
71
+ end
72
+
73
+ def reorder(proposals)
74
+ case order
75
+ when "random"
76
+ Proposal.transaction do
77
+ Proposal.connection.execute("SELECT setseed(#{Proposal.connection.quote(random_seed)})")
78
+ proposals.order("RANDOM()").load
79
+ end
80
+ when "most_voted"
81
+ proposals.order(proposal_votes_count: :desc)
82
+ when "recent"
83
+ proposals.order(created_at: :desc)
84
+ else
85
+ proposals
86
+ end
87
+ end
88
+
53
89
  def search_klass
54
90
  ProposalSearch
55
91
  end
@@ -61,7 +97,8 @@ module Decidim
61
97
  activity: "",
62
98
  category_id: "",
63
99
  state: "all",
64
- random_seed: params[:random_seed]
100
+ scope_id: "",
101
+ related_to: ""
65
102
  }
66
103
  end
67
104
  end
@@ -11,7 +11,9 @@ module Decidim
11
11
  attribute :scope_id, Integer
12
12
  attribute :user_group_id, Integer
13
13
 
14
- validates :title, :body, presence: true
14
+ validates :title, :body, presence: true, etiquette: true
15
+ validates :title, length: { maximum: 150 }
16
+ validates :body, length: { maximum: 500 }, etiquette: true
15
17
  validates :category, presence: true, if: ->(form) { form.category_id.present? }
16
18
  validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
17
19
 
@@ -27,7 +27,7 @@ module Decidim
27
27
  #
28
28
  # Returns true if the vote limit is enabled
29
29
  def vote_limit_enabled?
30
- current_user && feature_settings.vote_limit.present? && feature_settings.vote_limit.positive?
30
+ current_user && current_settings.votes_enabled? && feature_settings.vote_limit.present? && feature_settings.vote_limit.positive?
31
31
  end
32
32
 
33
33
  # Return the remaining votes for a user if the current feature has a vote limit
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ module Decidim
3
+ module Proposals
4
+ module Abilities
5
+ # Defines the abilities related to proposals for a logged in admin user.
6
+ # Intended to be used with `cancancan`.
7
+ class AdminUser
8
+ include CanCan::Ability
9
+
10
+ attr_reader :user, :context
11
+
12
+ def initialize(user, context)
13
+ return unless user && user.role?(:admin)
14
+
15
+ @user = user
16
+ @context = context
17
+
18
+ can :manage, Proposal
19
+ cannot :create, Proposal unless can_create_proposal?
20
+ cannot :update, Proposal unless can_update_proposal?
21
+ end
22
+
23
+ private
24
+
25
+ def current_settings
26
+ context.fetch(:current_settings, nil)
27
+ end
28
+
29
+ def feature_settings
30
+ context.fetch(:feature_settings, nil)
31
+ end
32
+
33
+ def can_create_proposal?
34
+ current_settings.try(:creation_enabled?) &&
35
+ feature_settings.try(:official_proposals_enabled)
36
+ end
37
+
38
+ def can_update_proposal?
39
+ current_settings.try(:proposal_answering_enabled) &&
40
+ feature_settings.try(:proposal_answering_enabled)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -16,27 +16,38 @@ module Decidim
16
16
  @context = context
17
17
 
18
18
  can :vote, Proposal do |_proposal|
19
- voting_enabled? && remaining_votes.positive?
19
+ authorized?(:vote) && voting_enabled? && remaining_votes.positive?
20
20
  end
21
21
 
22
22
  can :unvote, Proposal do |_proposal|
23
- voting_enabled? && vote_limit_enabled?
23
+ authorized?(:vote) && voting_enabled? && vote_limit_enabled?
24
24
  end
25
25
 
26
- can :create, Proposal if current_settings.try(:creation_enabled?)
26
+ can :create, Proposal if authorized?(:create) && creation_enabled?
27
27
  end
28
28
 
29
29
  private
30
30
 
31
+ def authorized?(action)
32
+ return unless feature
33
+
34
+ ActionAuthorizer.new(user, feature, action).authorize.ok?
35
+ end
36
+
31
37
  def vote_limit_enabled?
32
38
  return unless feature_settings
33
39
  feature_settings.vote_limit.present? && feature_settings.vote_limit.positive?
34
40
  end
35
41
 
42
+ def creation_enabled?
43
+ return unless current_settings
44
+ current_settings.creation_enabled?
45
+ end
46
+
36
47
  def remaining_votes
37
48
  return 1 unless vote_limit_enabled?
38
49
 
39
- proposals = Proposal.where(feature: context.fetch(:current_feature))
50
+ proposals = Proposal.where(feature: feature)
40
51
  votes_count = ProposalVote.where(author: user, proposal: proposals).size
41
52
  feature_settings.vote_limit - votes_count
42
53
  end
@@ -53,6 +64,13 @@ module Decidim
53
64
  def feature_settings
54
65
  context.fetch(:feature_settings, nil)
55
66
  end
67
+
68
+ def feature
69
+ feature = context.fetch(:current_feature, nil)
70
+ return nil unless feature && feature.manifest.name == :proposals
71
+
72
+ feature
73
+ end
56
74
  end
57
75
  end
58
76
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module Decidim
3
+ module Proposals
4
+ module Abilities
5
+ # Defines the abilities related to proposals for a logged in process admin user.
6
+ # Intended to be used with `cancancan`.
7
+ class ProcessAdminUser
8
+ include CanCan::Ability
9
+
10
+ attr_reader :user, :context
11
+
12
+ def initialize(user, context)
13
+ return unless user && !user.role?(:admin)
14
+
15
+ @user = user
16
+ @context = context
17
+
18
+ can :manage, Proposal do |proposal|
19
+ participatory_processes.include?(proposal.feature.participatory_process)
20
+ end
21
+
22
+ cannot :create, Proposal unless can_create_proposal?
23
+ cannot :update, Proposal unless can_update_proposal?
24
+ end
25
+
26
+ private
27
+
28
+ def current_settings
29
+ context.fetch(:current_settings, nil)
30
+ end
31
+
32
+ def feature_settings
33
+ context.fetch(:feature_settings, nil)
34
+ end
35
+
36
+ def current_feature
37
+ context.fetch(:current_feature, nil)
38
+ end
39
+
40
+ def can_create_proposal?
41
+ current_settings.try(:creation_enabled?) &&
42
+ feature_settings.try(:official_proposals_enabled) &&
43
+ participatory_processes.include?(current_feature.try(:participatory_process))
44
+ end
45
+
46
+ def can_update_proposal?
47
+ current_settings.try(:proposal_answering_enabled) &&
48
+ feature_settings.try(:proposal_answering_enabled)
49
+ end
50
+
51
+ def participatory_processes
52
+ @participatory_processes ||= Decidim::Admin::ManageableParticipatoryProcessesForUser.for(@user)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,10 +8,11 @@ module Decidim
8
8
  include Decidim::HasFeature
9
9
  include Decidim::HasScope
10
10
  include Decidim::HasCategory
11
+ include Decidim::Comments::Commentable
11
12
 
12
13
  feature_manifest_name "proposals"
13
14
 
14
- has_many :votes, foreign_key: "decidim_proposal_id", class_name: ProposalVote, dependent: :destroy
15
+ has_many :votes, foreign_key: "decidim_proposal_id", class_name: ProposalVote, dependent: :destroy, counter_cache: "proposal_votes_count"
15
16
 
16
17
  validates :title, :body, presence: true
17
18
 
@@ -30,7 +31,7 @@ module Decidim
30
31
  #
31
32
  # Returns Boolean.
32
33
  def voted_by?(user)
33
- votes.any? { |vote| vote.author == user }
34
+ votes.where(author: user).any?
34
35
  end
35
36
 
36
37
  # Public: Checks if the organization has given an answer for the proposal.
@@ -53,6 +54,26 @@ module Decidim
53
54
  def rejected?
54
55
  state == "rejected"
55
56
  end
57
+
58
+ # Public: Overrides the `commentable?` Commentable concern method.
59
+ def commentable?
60
+ feature.settings.comments_enabled?
61
+ end
62
+
63
+ # Public: Overrides the `accepts_new_comments?` Commentable concern method.
64
+ def accepts_new_comments?
65
+ commentable? && !feature.active_step_settings.comments_blocked
66
+ end
67
+
68
+ # Public: Overrides the `comments_have_alignment?` Commentable concern method.
69
+ def comments_have_alignment?
70
+ true
71
+ end
72
+
73
+ # Public: Overrides the `comments_have_votes?` Commentable concern method.
74
+ def comments_have_votes?
75
+ true
76
+ end
56
77
  end
57
78
  end
58
79
  end
@@ -8,10 +8,8 @@ module Decidim
8
8
  # feature - A Decidim::Feature to get the proposals from.
9
9
  # page - The page number to paginate the results.
10
10
  # per_page - The number of proposals to return per page.
11
- # random_seed - A random flaot number between -1 and 1 to be used as a random seed at the database.
12
11
  def initialize(options = {})
13
12
  super(Proposal.all, options)
14
- @random_seed = options[:random_seed].to_f
15
13
  end
16
14
 
17
15
  # Handle the search_text filter
@@ -46,6 +44,11 @@ module Decidim
46
44
  end
47
45
  end
48
46
 
47
+ # Handle the scope_id filter
48
+ def search_scope_id
49
+ query.where(decidim_scope_id: scope_id)
50
+ end
51
+
49
52
  # Handle the state filter
50
53
  def search_state
51
54
  if state == "accepted"
@@ -57,18 +60,27 @@ module Decidim
57
60
  end
58
61
  end
59
62
 
60
- # Returns the random proposals for the current page.
61
- def results
62
- @proposals ||= Proposal.transaction do
63
- Proposal.connection.execute("SELECT setseed(#{Proposal.connection.quote(random_seed)})")
64
- super.reorder("RANDOM()").load
65
- end
66
- end
63
+ # Filters Proposals by the name of the classes they are linked to. By default,
64
+ # returns all Proposals. When a `related_to` param is given, then it camelcases item
65
+ # to find the real class name and checks the links for the Proposals.
66
+ #
67
+ # The `related_to` param is expected to be in this form:
68
+ #
69
+ # "decidim/meetings/meeting"
70
+ #
71
+ # This can be achieved by performing `klass.name.underscore`.
72
+ #
73
+ # Returns only those proposals that are linked to the given class name.
74
+ def search_related_to
75
+ from = query
76
+ .joins(:resource_links_from)
77
+ .where(decidim_resource_links: { to_type: related_to.camelcase })
78
+
79
+ to = query
80
+ .joins(:resource_links_to)
81
+ .where(decidim_resource_links: { from_type: related_to.camelcase })
67
82
 
68
- # Returns the random seed used to randomize the proposals.
69
- def random_seed
70
- @random_seed = (rand * 2 - 1) if @random_seed == 0.0 || @random_seed > 1 || @random_seed < -1
71
- @random_seed
83
+ query.where(id: from).or(query.where(id: to))
72
84
  end
73
85
  end
74
86
  end
@@ -1,6 +1,6 @@
1
1
  <h3><%= t ".title", title: proposal.title %></h3>
2
2
 
3
- <%= form_for(@form, url: proposal_proposal_answer_path(proposal, @form)) do |f| %>
3
+ <%= decidim_form_for(@form, url: proposal_proposal_answer_path(proposal, @form)) do |f| %>
4
4
  <div class="field">
5
5
  <%= f.collection_radio_buttons :state, [["accepted", t('.accepted')], ["rejected", t('.rejected')]], :first, :last, prompt: true %>
6
6
  </div>
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
  <% end %>
14
14
 
15
- <% if @form.scopes&.any? %>
15
+ <% if @form.scopes&.any? && feature_settings.scoped_proposals_enabled %>
16
16
  <div class="field">
17
17
  <%= form.select :scope_id, @form.scopes.map{|s| [s.name, s.id]}, prompt: t(".select_a_scope") %>
18
18
  </div>
@@ -1,15 +1,19 @@
1
1
  <h2><%= t(".title") %></h2>
2
2
 
3
- <div class="actions title">
4
- <%= link_to t("actions.new", scope: "decidim.proposals", name: t("models.proposal.name", scope: "decidim.proposals.admin")), new_proposal_path, class: 'new' if can? :manage, current_feature %>
5
- </div>
3
+ <% if feature_settings.official_proposals_enabled %>
4
+ <div class="actions title">
5
+ <%= link_to t("actions.new", scope: "decidim.proposals", name: t("models.proposal.name", scope: "decidim.proposals.admin")), new_proposal_path, class: 'new' if can? :manage, current_feature %>
6
+ </div>
7
+ <% end %>
6
8
 
7
9
  <table class="stack">
8
10
  <thead>
9
11
  <tr>
10
12
  <th><%= t("models.proposal.fields.title", scope: "decidim.proposals") %></th>
11
13
  <th><%= t("models.proposal.fields.category", scope: "decidim.proposals") %></th>
12
- <th><%= t("models.proposal.fields.scope", scope: "decidim.proposals") %></th>
14
+ <% if feature_settings.scoped_proposals_enabled %>
15
+ <th><%= t("models.proposal.fields.scope", scope: "decidim.proposals") %></th>
16
+ <% end %>
13
17
  <th><%= t("models.proposal.fields.state", scope: "decidim.proposals") %></th>
14
18
  <th class="actions"><%= t("actions.title", scope: "decidim.proposals") %></th>
15
19
  </tr>
@@ -25,16 +29,18 @@
25
29
  <%= translated_attribute proposal.category.name %>
26
30
  <% end %>
27
31
  </td>
28
- <td>
29
- <% if proposal.scope %>
30
- <%= translated_attribute proposal.scope.name %>
31
- <% end %>
32
- </td>
32
+ <% if feature_settings.scoped_proposals_enabled %>
33
+ <td>
34
+ <% if proposal.scope %>
35
+ <%= translated_attribute proposal.scope.name %>
36
+ <% end %>
37
+ </td>
38
+ <% end %>
33
39
  <td>
34
40
  <%= humanize_proposal_state proposal.state %>
35
41
  </td>
36
42
  <td class="actions">
37
- <%= link_to t("actions.answer", scope: "decidim.proposals"), edit_proposal_proposal_answer_path(proposal_id: proposal.id, id: proposal.id) if can? :update, current_feature %>
43
+ <%= link_to t("actions.answer", scope: "decidim.proposals"), edit_proposal_proposal_answer_path(proposal_id: proposal.id, id: proposal.id) if can? :update, proposal %>
38
44
  </td>
39
45
  </tr>
40
46
  <% end %>