decidim-proposals 0.0.2 → 0.0.3
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/assets/stylesheets/decidim/proposals/social_share.css.scss +5 -0
- data/app/commands/decidim/proposals/admin/answer_proposal.rb +43 -0
- data/app/commands/decidim/proposals/create_proposal.rb +1 -0
- data/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +37 -0
- data/app/controllers/decidim/proposals/admin/proposals_controller.rb +1 -0
- data/app/controllers/decidim/proposals/proposal_votes_controller.rb +18 -7
- data/app/controllers/decidim/proposals/proposals_controller.rb +8 -7
- data/app/forms/decidim/proposals/admin/proposal_answer_form.rb +18 -0
- data/app/forms/decidim/proposals/proposal_form.rb +1 -0
- data/app/helpers/decidim/proposals/application_helper.rb +17 -0
- data/app/helpers/decidim/proposals/proposal_votes_helper.rb +19 -0
- data/app/models/decidim/proposals/abilities/current_user.rb +59 -0
- data/app/models/decidim/proposals/proposal.rb +30 -33
- data/app/models/decidim/proposals/proposal_vote.rb +2 -0
- data/app/services/decidim/proposals/proposal_search.rb +11 -0
- data/app/views/decidim/proposals/admin/proposal_answers/edit.html.erb +15 -0
- data/app/views/decidim/proposals/admin/proposals/index.html.erb +8 -0
- data/app/views/decidim/proposals/proposal_votes/update_buttons_and_counters.js.erb +20 -0
- data/app/views/decidim/proposals/proposals/_filters.html.erb +3 -1
- data/app/views/decidim/proposals/proposals/_filters_small_view.html.erb +18 -0
- data/app/views/decidim/proposals/proposals/_linked_proposals.html.erb +27 -0
- data/app/views/decidim/proposals/proposals/_proposal.html.erb +3 -2
- data/app/views/decidim/proposals/proposals/_proposal_badge.html.erb +3 -0
- data/app/views/decidim/proposals/proposals/_remaining_votes_count.html.erb +1 -0
- data/app/views/decidim/proposals/proposals/_share.html.erb +1 -1
- data/app/views/decidim/proposals/proposals/_vote_button.html.erb +16 -4
- data/app/views/decidim/proposals/proposals/_votes_limit.html.erb +23 -0
- data/app/views/decidim/proposals/proposals/index.html.erb +13 -3
- data/app/views/decidim/proposals/proposals/new.html.erb +6 -0
- data/app/views/decidim/proposals/proposals/show.html.erb +15 -2
- data/config/i18n-tasks.yml +3 -0
- data/config/locales/ca.yml +54 -1
- data/config/locales/en.yml +54 -0
- data/config/locales/es.yml +54 -1
- data/db/migrate/20170120151202_add_user_group_id_to_proposals.rb +5 -0
- data/db/migrate/20170131092413_add_answers_to_proposals.rb +7 -0
- data/lib/decidim/proposals/admin_engine.rb +3 -1
- data/lib/decidim/proposals/engine.rb +7 -1
- data/lib/decidim/proposals/feature.rb +28 -1
- data/lib/decidim/proposals/test/factories.rb +83 -0
- metadata +64 -9
- data/app/views/decidim/proposals/proposal_votes/create.js.erb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d9615abcde37945383d87bb3f7c74126d66b3c
|
4
|
+
data.tar.gz: 588e35fae074a55307aab6789d6d3af25db87d9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a883d5c8a999da8fb3f0bda1811678905858d1693f69b846b975b66f2c25cbdbf0bfa7c671ceb991519f2b68972ae2cef71bd08be7a21f03cda07a5dfbe6cc9e
|
7
|
+
data.tar.gz: d0882dff25bb3c47491036de7988d4e1fd392e6d1806e49a287ebcd4cf02e9e70a6a64b829e07102e16607ea31363ba17afb1dbed0968450df66e02ba2033330
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Proposals
|
4
|
+
module Admin
|
5
|
+
# A command with all the business logic when an admin answers a proposal.
|
6
|
+
class AnswerProposal < Rectify::Command
|
7
|
+
# Public: Initializes the command.
|
8
|
+
#
|
9
|
+
# form - A form object with the params.
|
10
|
+
# proposal - The proposal to write the answer for.
|
11
|
+
def initialize(form, proposal)
|
12
|
+
@form = form
|
13
|
+
@proposal = proposal
|
14
|
+
end
|
15
|
+
|
16
|
+
# Executes the command. Broadcasts these events:
|
17
|
+
#
|
18
|
+
# - :ok when everything is valid.
|
19
|
+
# - :invalid if the form wasn't valid and we couldn't proceed.
|
20
|
+
#
|
21
|
+
# Returns nothing.
|
22
|
+
def call
|
23
|
+
return broadcast(:invalid) if form.invalid?
|
24
|
+
|
25
|
+
answer_proposal
|
26
|
+
broadcast(:ok)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :form, :proposal
|
32
|
+
|
33
|
+
def answer_proposal
|
34
|
+
proposal.update_attributes!(
|
35
|
+
state: @form.state,
|
36
|
+
answer: @form.answer,
|
37
|
+
answered_at: Time.current
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Proposals
|
4
|
+
module Admin
|
5
|
+
# This controller allows admins to answer proposals in a participatory process.
|
6
|
+
class ProposalAnswersController < Admin::ApplicationController
|
7
|
+
helper_method :proposal
|
8
|
+
|
9
|
+
def edit
|
10
|
+
@form = form(Admin::ProposalAnswerForm).from_model(proposal)
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
@form = form(Admin::ProposalAnswerForm).from_params(params)
|
15
|
+
|
16
|
+
Admin::AnswerProposal.call(@form, proposal) do
|
17
|
+
on(:ok) do
|
18
|
+
flash[:notice] = I18n.t("proposals.answer.success", scope: "decidim.proposals.admin")
|
19
|
+
redirect_to proposals_path
|
20
|
+
end
|
21
|
+
|
22
|
+
on(:invalid) do
|
23
|
+
flash.now[:alert] = I18n.t("proposals.answer.invalid", scope: "decidim.proposals.admin")
|
24
|
+
render action: "edit"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def proposal
|
32
|
+
@proposals ||= Proposal.where(feature: current_feature).find(params[:id])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -4,21 +4,32 @@ module Decidim
|
|
4
4
|
module Proposals
|
5
5
|
# Exposes the proposal vote resource so users can vote proposals.
|
6
6
|
class ProposalVotesController < Decidim::Proposals::ApplicationController
|
7
|
+
include ProposalVotesHelper
|
8
|
+
|
9
|
+
helper_method :proposal
|
10
|
+
|
7
11
|
before_action :authenticate_user!
|
8
|
-
before_action :check_current_settings!
|
9
12
|
|
10
13
|
def create
|
11
|
-
|
12
|
-
|
14
|
+
authorize! :vote, proposal
|
15
|
+
|
16
|
+
proposal.votes.create!(author: current_user)
|
17
|
+
@from_proposals_list = params[:from_proposals_list] == "true"
|
18
|
+
render :update_buttons_and_counters
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
authorize! :unvote, proposal
|
23
|
+
|
24
|
+
proposal.votes.where(author: current_user).delete_all
|
13
25
|
@from_proposals_list = params[:from_proposals_list] == "true"
|
26
|
+
render :update_buttons_and_counters
|
14
27
|
end
|
15
28
|
|
16
29
|
private
|
17
30
|
|
18
|
-
|
19
|
-
|
20
|
-
def check_current_settings!
|
21
|
-
raise "This setting is not enabled for this step" unless current_settings.votes_enabled?
|
31
|
+
def proposal
|
32
|
+
@proposal ||= Proposal.where(feature: current_feature).find(params[:proposal_id])
|
22
33
|
end
|
23
34
|
end
|
24
35
|
end
|
@@ -18,14 +18,21 @@ module Decidim
|
|
18
18
|
.results
|
19
19
|
.includes(:author)
|
20
20
|
.includes(votes: [:author])
|
21
|
+
.page(params[:page])
|
22
|
+
.per(12)
|
23
|
+
|
21
24
|
@random_seed = search.random_seed
|
22
25
|
end
|
23
26
|
|
24
27
|
def new
|
28
|
+
authorize! :create, Proposal
|
29
|
+
|
25
30
|
@form = form(ProposalForm).from_params({})
|
26
31
|
end
|
27
32
|
|
28
33
|
def create
|
34
|
+
authorize! :create, Proposal
|
35
|
+
|
29
36
|
@form = form(ProposalForm).from_params(params)
|
30
37
|
|
31
38
|
CreateProposal.call(@form, current_user) do
|
@@ -47,19 +54,13 @@ module Decidim
|
|
47
54
|
ProposalSearch
|
48
55
|
end
|
49
56
|
|
50
|
-
def default_search_params
|
51
|
-
{
|
52
|
-
page: params[:page],
|
53
|
-
per_page: 12
|
54
|
-
}
|
55
|
-
end
|
56
|
-
|
57
57
|
def default_filter_params
|
58
58
|
{
|
59
59
|
search_text: "",
|
60
60
|
origin: "all",
|
61
61
|
activity: "",
|
62
62
|
category_id: "",
|
63
|
+
state: "all",
|
63
64
|
random_seed: params[:random_seed]
|
64
65
|
}
|
65
66
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Decidim
|
3
|
+
module Proposals
|
4
|
+
module Admin
|
5
|
+
# A form object to be used when admin users want to answer a proposal.
|
6
|
+
class ProposalAnswerForm < Decidim::Form
|
7
|
+
include TranslatableAttributes
|
8
|
+
mimic :proposal_answer
|
9
|
+
|
10
|
+
translatable_attribute :answer, String
|
11
|
+
attribute :state, String
|
12
|
+
|
13
|
+
validates :state, presence: true, inclusion: { in: %w(accepted rejected) }
|
14
|
+
validates :answer, translatable_presence: true, if: ->(form) { form.state == "rejected" }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -9,6 +9,7 @@ module Decidim
|
|
9
9
|
attribute :body, String
|
10
10
|
attribute :category_id, Integer
|
11
11
|
attribute :scope_id, Integer
|
12
|
+
attribute :user_group_id, Integer
|
12
13
|
|
13
14
|
validates :title, :body, presence: true
|
14
15
|
validates :category, presence: true, if: ->(form) { form.category_id.present? }
|
@@ -7,6 +7,23 @@ module Decidim
|
|
7
7
|
include Decidim::Comments::CommentsHelper
|
8
8
|
include PaginateHelper
|
9
9
|
include ProposalVotesHelper
|
10
|
+
|
11
|
+
# Public: The state of a proposal in a way a human can understand.
|
12
|
+
#
|
13
|
+
# state - The String state of the proposal.
|
14
|
+
#
|
15
|
+
# Returns a String.
|
16
|
+
def humanize_proposal_state(state)
|
17
|
+
value = if state == "accepted"
|
18
|
+
"accepted"
|
19
|
+
elsif state == "rejected"
|
20
|
+
"rejected"
|
21
|
+
else
|
22
|
+
"not_answered"
|
23
|
+
end
|
24
|
+
|
25
|
+
I18n.t(value, scope: "decidim.proposals.answers")
|
26
|
+
end
|
10
27
|
end
|
11
28
|
end
|
12
29
|
end
|
@@ -22,6 +22,25 @@ module Decidim
|
|
22
22
|
return "small" if from_proposals_list
|
23
23
|
"expanded button--sc"
|
24
24
|
end
|
25
|
+
|
26
|
+
# Check if the vote limit is enabled for the current feature
|
27
|
+
#
|
28
|
+
# Returns true if the vote limit is enabled
|
29
|
+
def vote_limit_enabled?
|
30
|
+
current_user && feature_settings.vote_limit.present? && feature_settings.vote_limit.positive?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return the remaining votes for a user if the current feature has a vote limit
|
34
|
+
#
|
35
|
+
# user - A User object
|
36
|
+
#
|
37
|
+
# Returns a number with the remaining votes for that user
|
38
|
+
def remaining_votes_count_for(user)
|
39
|
+
return 0 unless vote_limit_enabled?
|
40
|
+
proposals = Proposal.where(feature: current_feature)
|
41
|
+
votes_count = ProposalVote.where(author: user, proposal: proposals).size
|
42
|
+
feature_settings.vote_limit - votes_count
|
43
|
+
end
|
25
44
|
end
|
26
45
|
end
|
27
46
|
end
|
@@ -0,0 +1,59 @@
|
|
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 user.
|
6
|
+
# Intended to be used with `cancancan`.
|
7
|
+
class CurrentUser
|
8
|
+
include CanCan::Ability
|
9
|
+
|
10
|
+
attr_reader :user, :context
|
11
|
+
|
12
|
+
def initialize(user, context)
|
13
|
+
return unless user
|
14
|
+
|
15
|
+
@user = user
|
16
|
+
@context = context
|
17
|
+
|
18
|
+
can :vote, Proposal do |_proposal|
|
19
|
+
voting_enabled? && remaining_votes.positive?
|
20
|
+
end
|
21
|
+
|
22
|
+
can :unvote, Proposal do |_proposal|
|
23
|
+
voting_enabled? && vote_limit_enabled?
|
24
|
+
end
|
25
|
+
|
26
|
+
can :create, Proposal if current_settings.try(:creation_enabled?)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def vote_limit_enabled?
|
32
|
+
return unless feature_settings
|
33
|
+
feature_settings.vote_limit.present? && feature_settings.vote_limit.positive?
|
34
|
+
end
|
35
|
+
|
36
|
+
def remaining_votes
|
37
|
+
return 1 unless vote_limit_enabled?
|
38
|
+
|
39
|
+
proposals = Proposal.where(feature: context.fetch(:current_feature))
|
40
|
+
votes_count = ProposalVote.where(author: user, proposal: proposals).size
|
41
|
+
feature_settings.vote_limit - votes_count
|
42
|
+
end
|
43
|
+
|
44
|
+
def voting_enabled?
|
45
|
+
return unless current_settings
|
46
|
+
current_settings.votes_enabled? && !current_settings.votes_blocked?
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_settings
|
50
|
+
context.fetch(:current_settings, nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
def feature_settings
|
54
|
+
context.fetch(:feature_settings, nil)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -3,58 +3,55 @@ module Decidim
|
|
3
3
|
module Proposals
|
4
4
|
# The data store for a Proposal in the Decidim::Proposals component.
|
5
5
|
class Proposal < Proposals::ApplicationRecord
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
include Decidim::Resourceable
|
7
|
+
include Decidim::Authorable
|
8
|
+
include Decidim::HasFeature
|
9
|
+
include Decidim::HasScope
|
10
|
+
include Decidim::HasCategory
|
11
|
+
|
12
|
+
feature_manifest_name "proposals"
|
13
|
+
|
11
14
|
has_many :votes, foreign_key: "decidim_proposal_id", class_name: ProposalVote, dependent: :destroy
|
12
15
|
|
13
|
-
validates :title, :
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
validates :title, :body, presence: true
|
17
|
+
|
18
|
+
scope :accepted, -> { where(state: "accepted") }
|
19
|
+
scope :rejected, -> { where(state: "rejected") }
|
17
20
|
|
18
21
|
def author_name
|
19
|
-
author&.name || I18n.t("decidim.proposals.models.proposal.fields.official_proposal")
|
22
|
+
user_group&.name || author&.name || I18n.t("decidim.proposals.models.proposal.fields.official_proposal")
|
20
23
|
end
|
21
24
|
|
22
25
|
def author_avatar_url
|
23
26
|
author&.avatar&.url || ActionController::Base.helpers.asset_path("decidim/default-avatar.svg")
|
24
27
|
end
|
25
28
|
|
26
|
-
# Public:
|
29
|
+
# Public: Check if the user has voted the proposal.
|
27
30
|
#
|
28
|
-
#
|
29
|
-
# assume all proposals can be commented.
|
30
|
-
#
|
31
|
-
# Returns Boolean
|
32
|
-
def commentable?
|
33
|
-
true
|
34
|
-
end
|
35
|
-
|
36
|
-
# Public: Check if the user has voted the proposal
|
37
|
-
#
|
38
|
-
# Returns Boolean
|
31
|
+
# Returns Boolean.
|
39
32
|
def voted_by?(user)
|
40
33
|
votes.any? { |vote| vote.author == user }
|
41
34
|
end
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
36
|
+
# Public: Checks if the organization has given an answer for the proposal.
|
37
|
+
#
|
38
|
+
# Returns Boolean.
|
39
|
+
def answered?
|
40
|
+
answered_at.present?
|
48
41
|
end
|
49
42
|
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
# Public: Checks if the organization has accepted a proposal.
|
44
|
+
#
|
45
|
+
# Returns Boolean.
|
46
|
+
def accepted?
|
47
|
+
state == "accepted"
|
53
48
|
end
|
54
49
|
|
55
|
-
|
56
|
-
|
57
|
-
|
50
|
+
# Public: Checks if the organization has rejected a proposal.
|
51
|
+
#
|
52
|
+
# Returns Boolean.
|
53
|
+
def rejected?
|
54
|
+
state == "rejected"
|
58
55
|
end
|
59
56
|
end
|
60
57
|
end
|
@@ -6,6 +6,7 @@ module Decidim
|
|
6
6
|
belongs_to :proposal, foreign_key: "decidim_proposal_id", class_name: Decidim::Proposals::Proposal, counter_cache: true
|
7
7
|
belongs_to :author, foreign_key: "decidim_author_id", class_name: Decidim::User
|
8
8
|
|
9
|
+
validates :proposal, :author, presence: true
|
9
10
|
validates :proposal, uniqueness: { scope: :author }
|
10
11
|
validate :author_and_proposal_same_organization
|
11
12
|
|
@@ -13,6 +14,7 @@ module Decidim
|
|
13
14
|
|
14
15
|
# Private: check if the proposal and the author have the same organization
|
15
16
|
def author_and_proposal_same_organization
|
17
|
+
return if !proposal || !author
|
16
18
|
errors.add(:proposal, :invalid) unless author.organization == proposal.organization
|
17
19
|
end
|
18
20
|
end
|