decidim-proposals 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|