decidim-proposals 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/app/assets/config/decidim_proposals_manifest.css +1 -0
  4. data/app/assets/config/decidim_proposals_manifest.js +1 -0
  5. data/app/assets/images/decidim/proposals/icon.svg +1 -0
  6. data/app/assets/javascripts/decidim/proposals/social_share.js +2 -0
  7. data/app/assets/stylesheets/decidim/proposals/social_share.css.scss +14 -0
  8. data/app/commands/decidim/proposals/create_proposal.rb +5 -3
  9. data/app/controllers/decidim/proposals/admin/proposals_controller.rb +3 -3
  10. data/app/controllers/decidim/proposals/proposal_votes_controller.rb +25 -0
  11. data/app/controllers/decidim/proposals/proposals_controller.rb +33 -6
  12. data/app/forms/decidim/proposals/admin/proposal_form.rb +7 -6
  13. data/app/forms/decidim/proposals/proposal_form.rb +6 -6
  14. data/app/helpers/decidim/proposals/application_helper.rb +3 -1
  15. data/app/helpers/decidim/proposals/proposal_votes_helper.rb +27 -0
  16. data/app/models/decidim/proposals/proposal.rb +8 -0
  17. data/app/models/decidim/proposals/proposal_vote.rb +20 -0
  18. data/app/services/decidim/proposals/proposal_search.rb +39 -11
  19. data/app/views/decidim/proposals/admin/proposals/_form.html.erb +2 -2
  20. data/app/views/decidim/proposals/proposal_votes/create.js.erb +5 -0
  21. data/app/views/decidim/proposals/proposals/_count.html.erb +1 -0
  22. data/app/views/decidim/proposals/proposals/_filters.html.erb +26 -0
  23. data/app/views/decidim/proposals/proposals/_proposal.html.erb +11 -2
  24. data/app/views/decidim/proposals/proposals/_proposals.html.erb +4 -0
  25. data/app/views/decidim/proposals/proposals/_share.html.erb +33 -0
  26. data/app/views/decidim/proposals/proposals/_vote_button.html.erb +13 -0
  27. data/app/views/decidim/proposals/proposals/_votes_count.html.erb +6 -0
  28. data/app/views/decidim/proposals/proposals/index.html.erb +11 -11
  29. data/app/views/decidim/proposals/proposals/index.js.erb +5 -0
  30. data/app/views/decidim/proposals/proposals/show.html.erb +31 -4
  31. data/config/i18n-tasks.yml +1 -0
  32. data/config/initializers/social_share_button.rb +4 -0
  33. data/config/locales/ca.yml +28 -2
  34. data/config/locales/en.yml +27 -1
  35. data/config/locales/es.yml +28 -2
  36. data/db/migrate/20170112115253_create_proposal_votes.rb +12 -0
  37. data/db/migrate/20170113114245_add_text_search_indexes.rb +6 -0
  38. data/db/migrate/20170118120151_add_counter_cache_votes_to_proposals.rb +5 -0
  39. data/lib/decidim/proposals/engine.rb +8 -1
  40. data/lib/decidim/proposals/feature.rb +30 -4
  41. metadata +45 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4be5f073d284da3f8ac70f23505dc51a15b6b888
4
- data.tar.gz: b6e8be99a9db14b98eec0246f41648d34bbf3d84
3
+ metadata.gz: f981594ab6257e9cf721a9ffd76368d8c86c96f8
4
+ data.tar.gz: e857387adb235b90d3f758f57565712fccca1334
5
5
  SHA512:
6
- metadata.gz: e7d3f307976ba987e3eb993452f98e58bedb727ae06a7caa81fd0fd0b7a0afc24f59950f999db00a41c103984b4a1d188703ea8d70dc808976a24b270ecd0867
7
- data.tar.gz: 82905c51a3d79b6fb87579567dbb717fc3e359ff4088c2992fc1e53ba5341e77411e53f0c1f25f4d5a0b23fdee426cd75d38b001973cbde4cc6d6b96e8d06812
6
+ metadata.gz: 6ed1c5ea32849f63efe358f438fc878b833528030d6d12d0669953e5419866c8a24a5c1f9dfb2c56f76e6f854a19487920c4bc0354f8db5853bfdc48aa7c9717
7
+ data.tar.gz: efb15ffb3ad5e9fbfea35fa780a0966e9b5d44899c7c8dde74dd1ce418bea634b268b2a9e7faf5cbfd349b067139add3f128dabd06cbdfca3428aedb3ffbd134
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
1
  # frozen_string_literal: true
2
- require "decidim/common_rake"
2
+ require "decidim/dev/common_rake"
@@ -0,0 +1 @@
1
+ /*= link decidim/proposals/social_share.css.scss */
@@ -0,0 +1 @@
1
+ //= link decidim/proposals/social_share.js
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35"><path d="M17.5 35A17.5 17.5 0 1 1 35 17.5 17.52 17.52 0 0 1 17.5 35zm0-33.06A15.56 15.56 0 1 0 33.06 17.5 15.57 15.57 0 0 0 17.5 1.94zm9.5 13.7H8a1 1 0 0 1 0-1.94h19a1 1 0 0 1 0 1.94zm0 3.68H8a1 1 0 0 1 0-1.94h19a1 1 0 0 1 0 1.94zM22.26 23H8a1 1 0 0 1 0-1.94h14.26a1 1 0 0 1 0 1.94z"/></svg>
@@ -0,0 +1,2 @@
1
+ // = require social-share-button
2
+ // = require_self
@@ -0,0 +1,14 @@
1
+ /*= require social-share-button */
2
+ $size: 45px;
3
+
4
+ .social-share-button {
5
+ display: inline-block;
6
+ vertical-align: top;
7
+ .ssb-icon {
8
+ margin-right: 5px;
9
+ background-size: $size $size;
10
+ height: $size;
11
+ width: $size;
12
+ }
13
+ }
14
+
@@ -5,9 +5,11 @@ module Decidim
5
5
  class CreateProposal < Rectify::Command
6
6
  # Public: Initializes the command.
7
7
  #
8
- # form - A form object with the params.
9
- def initialize(form)
8
+ # form - A form object with the params.
9
+ # current_user - The current user.
10
+ def initialize(form, current_user)
10
11
  @form = form
12
+ @current_user = current_user
11
13
  end
12
14
 
13
15
  # Executes the command. Broadcasts these events:
@@ -33,7 +35,7 @@ module Decidim
33
35
  body: form.body,
34
36
  category: form.category,
35
37
  scope: form.scope,
36
- author: form.author,
38
+ author: @current_user,
37
39
  feature: form.feature
38
40
  )
39
41
  end
@@ -7,13 +7,13 @@ module Decidim
7
7
  helper_method :proposals
8
8
 
9
9
  def new
10
- @form = form(ProposalForm).from_params({}, feature: current_feature)
10
+ @form = form(Admin::ProposalForm).from_params({})
11
11
  end
12
12
 
13
13
  def create
14
- @form = form(ProposalForm).from_params(params, feature: current_feature)
14
+ @form = form(Admin::ProposalForm).from_params(params)
15
15
 
16
- CreateProposal.call(@form) do
16
+ Admin::CreateProposal.call(@form) do
17
17
  on(:ok) do
18
18
  flash[:notice] = I18n.t("proposals.create.success", scope: "decidim.proposals.admin")
19
19
  redirect_to proposals_path
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Proposals
5
+ # Exposes the proposal vote resource so users can vote proposals.
6
+ class ProposalVotesController < Decidim::Proposals::ApplicationController
7
+ before_action :authenticate_user!
8
+ before_action :check_current_settings!
9
+
10
+ def create
11
+ @proposal = Proposal.where(feature: current_feature).find(params[:proposal_id])
12
+ @proposal.votes.create!(author: current_user)
13
+ @from_proposals_list = params[:from_proposals_list] == "true"
14
+ end
15
+
16
+ private
17
+
18
+ # The vote buttons should not be visible if the setting is not enabled.
19
+ # This ensure the votes cannot be created using a POST request directly.
20
+ def check_current_settings!
21
+ raise "This setting is not enabled for this step" unless current_settings.votes_enabled?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,6 +5,8 @@ module Decidim
5
5
  # Exposes the proposal resource so users can view and create them.
6
6
  class ProposalsController < Decidim::Proposals::ApplicationController
7
7
  include FormFactory
8
+ include FilterResource
9
+
8
10
  before_action :authenticate_user!, only: [:new, :create]
9
11
 
10
12
  def show
@@ -12,19 +14,21 @@ module Decidim
12
14
  end
13
15
 
14
16
  def index
15
- @search = ProposalSearch.new(current_feature, params[:page], params[:random_seed])
16
- @proposals = @search.proposals
17
- @random_seed = @search.random_seed
17
+ @proposals = search
18
+ .results
19
+ .includes(:author)
20
+ .includes(votes: [:author])
21
+ @random_seed = search.random_seed
18
22
  end
19
23
 
20
24
  def new
21
- @form = form(ProposalForm).from_params({}, author: current_user, feature: current_feature)
25
+ @form = form(ProposalForm).from_params({})
22
26
  end
23
27
 
24
28
  def create
25
- @form = form(ProposalForm).from_params(params, author: current_user, feature: current_feature)
29
+ @form = form(ProposalForm).from_params(params)
26
30
 
27
- CreateProposal.call(@form) do
31
+ CreateProposal.call(@form, current_user) do
28
32
  on(:ok) do |proposal|
29
33
  flash[:notice] = I18n.t("proposals.create.success", scope: "decidim")
30
34
  redirect_to proposal_path(proposal)
@@ -36,6 +40,29 @@ module Decidim
36
40
  end
37
41
  end
38
42
  end
43
+
44
+ private
45
+
46
+ def search_klass
47
+ ProposalSearch
48
+ end
49
+
50
+ def default_search_params
51
+ {
52
+ page: params[:page],
53
+ per_page: 12
54
+ }
55
+ end
56
+
57
+ def default_filter_params
58
+ {
59
+ search_text: "",
60
+ origin: "all",
61
+ activity: "",
62
+ category_id: "",
63
+ random_seed: params[:random_seed]
64
+ }
65
+ end
39
66
  end
40
67
  end
41
68
  end
@@ -10,27 +10,28 @@ module Decidim
10
10
  attribute :body, String
11
11
  attribute :category_id, Integer
12
12
  attribute :scope_id, Integer
13
- attribute :feature, Decidim::Feature
14
13
 
15
- validates :title, :body, :feature, presence: true
14
+ validates :title, :body, presence: true
16
15
  validates :category, presence: true, if: ->(form) { form.category_id.present? }
17
16
  validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
18
17
 
19
- delegate :categories, to: :feature
20
- delegate :scopes, to: :current_organization
18
+ delegate :categories, to: :current_feature, prefix: false
19
+ delegate :scopes, to: :current_organization, prefix: false
20
+
21
+ alias feature current_feature
21
22
 
22
23
  # Finds the Category from the category_id.
23
24
  #
24
25
  # Returns a Decidim::Category
25
26
  def category
26
- @category ||= feature.categories.where(id: category_id).first
27
+ @category ||= categories.where(id: category_id).first
27
28
  end
28
29
 
29
30
  # Finds the Scope from the scope_id.
30
31
  #
31
32
  # Returns a Decidim::Scope
32
33
  def scope
33
- @scope ||= feature.scopes.where(id: scope_id).first
34
+ @scope ||= scopes.where(id: scope_id).first
34
35
  end
35
36
  end
36
37
  end
@@ -7,30 +7,30 @@ module Decidim
7
7
 
8
8
  attribute :title, String
9
9
  attribute :body, String
10
- attribute :author, Decidim::User
11
10
  attribute :category_id, Integer
12
11
  attribute :scope_id, Integer
13
- attribute :feature, Decidim::Feature
14
12
 
15
- validates :title, :body, :author, :feature, presence: true
13
+ validates :title, :body, presence: true
16
14
  validates :category, presence: true, if: ->(form) { form.category_id.present? }
17
15
  validates :scope, presence: true, if: ->(form) { form.scope_id.present? }
18
16
 
19
- delegate :categories, to: :feature
17
+ delegate :categories, to: :current_feature
20
18
  delegate :scopes, to: :current_organization
21
19
 
20
+ alias feature current_feature
21
+
22
22
  # Finds the Category from the category_id.
23
23
  #
24
24
  # Returns a Decidim::Category
25
25
  def category
26
- @category ||= feature.categories.where(id: category_id).first
26
+ @category ||= categories.where(id: category_id).first
27
27
  end
28
28
 
29
29
  # Finds the Scope from the scope_id.
30
30
  #
31
31
  # Returns a Decidim::Scope
32
32
  def scope
33
- @scope ||= feature.scopes.where(id: scope_id).first
33
+ @scope ||= scopes.where(id: scope_id).first
34
34
  end
35
35
  end
36
36
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  module Decidim
3
3
  module Proposals
4
- # Custom helpers, scoped to the pages engine.
4
+ # Custom helpers, scoped to the proposals engine.
5
5
  #
6
6
  module ApplicationHelper
7
7
  include Decidim::Comments::CommentsHelper
8
+ include PaginateHelper
9
+ include ProposalVotesHelper
8
10
  end
9
11
  end
10
12
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ module Decidim
3
+ module Proposals
4
+ # Simple helpers to handle markup variations for proposal votes partials
5
+ module ProposalVotesHelper
6
+ # Returns the css classes used for proposal votes count in both proposals list and show pages
7
+ #
8
+ # from_proposals_list - A boolean to indicate if the template is rendered from the proposals list page
9
+ #
10
+ # Returns a hash with the css classes for the count number and label
11
+ def votes_count_classes(from_proposals_list)
12
+ return { number: "card__support__number", label: "" } if from_proposals_list
13
+ { number: "extra__suport-number", label: "extra__suport-text" }
14
+ end
15
+
16
+ # Returns the css classes used for proposal vote button in both proposals list and show pages
17
+ #
18
+ # from_proposals_list - A boolean to indicate if the template is rendered from the proposals list page
19
+ #
20
+ # Returns a string with the value of the css classes.
21
+ def vote_button_classes(from_proposals_list)
22
+ return "small" if from_proposals_list
23
+ "expanded button--sc"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -8,6 +8,7 @@ module Decidim
8
8
  belongs_to :category, foreign_key: "decidim_category_id", class_name: Decidim::Category
9
9
  belongs_to :scope, foreign_key: "decidim_scope_id", class_name: Decidim::Scope
10
10
  has_one :organization, through: :feature
11
+ has_many :votes, foreign_key: "decidim_proposal_id", class_name: ProposalVote, dependent: :destroy
11
12
 
12
13
  validates :title, :feature, :body, presence: true
13
14
  validate :category_belongs_to_feature
@@ -32,6 +33,13 @@ module Decidim
32
33
  true
33
34
  end
34
35
 
36
+ # Public: Check if the user has voted the proposal
37
+ #
38
+ # Returns Boolean
39
+ def voted_by?(user)
40
+ votes.any? { |vote| vote.author == user }
41
+ end
42
+
35
43
  private
36
44
 
37
45
  def category_belongs_to_feature
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module Decidim
3
+ module Proposals
4
+ # A proposal can include a vote per user.
5
+ class ProposalVote < ApplicationRecord
6
+ belongs_to :proposal, foreign_key: "decidim_proposal_id", class_name: Decidim::Proposals::Proposal, counter_cache: true
7
+ belongs_to :author, foreign_key: "decidim_author_id", class_name: Decidim::User
8
+
9
+ validates :proposal, uniqueness: { scope: :author }
10
+ validate :author_and_proposal_same_organization
11
+
12
+ private
13
+
14
+ # Private: check if the proposal and the author have the same organization
15
+ def author_and_proposal_same_organization
16
+ errors.add(:proposal, :invalid) unless author.organization == proposal.organization
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,26 +3,54 @@ module Decidim
3
3
  module Proposals
4
4
  # A service to encapsualte all the logic when searching and filtering
5
5
  # proposals in a participatory process.
6
- class ProposalSearch
7
- attr_reader :feature, :page, :per_page
8
-
6
+ class ProposalSearch < ResourceSearch
9
7
  # Public: Initializes the service.
10
8
  # feature - A Decidim::Feature to get the proposals from.
11
9
  # page - The page number to paginate the results.
12
- # random_seed - A random flaot number between -1 and 1 to be used as a random seed at the database.
13
10
  # per_page - The number of proposals to return per page.
14
- def initialize(feature, page = nil, random_seed = nil, per_page = nil)
15
- @feature = feature
16
- @page = (page || 1).to_i
17
- @per_page = (per_page || 12).to_i
18
- @random_seed = random_seed.to_f
11
+ # random_seed - A random flaot number between -1 and 1 to be used as a random seed at the database.
12
+ def initialize(options = {})
13
+ super(Proposal.all, options)
14
+ @random_seed = options[:random_seed].to_f
15
+ end
16
+
17
+ # Handle the search_text filter
18
+ def search_search_text
19
+ query
20
+ .where("title ILIKE ?", "%#{search_text}%")
21
+ .or(query.where("body ILIKE ?", "%#{search_text}%"))
22
+ end
23
+
24
+ # Handle the origin filter
25
+ # The 'official' proposals doesn't have an author id
26
+ def search_origin
27
+ if origin == "official"
28
+ query.where(decidim_author_id: nil)
29
+ elsif origin == "citizenship"
30
+ query.where.not(decidim_author_id: nil)
31
+ else # Assume 'all'
32
+ query
33
+ end
34
+ end
35
+
36
+ # Handle the activity filter
37
+ def search_activity
38
+ if activity.include? "voted"
39
+ query
40
+ .includes(:votes)
41
+ .where(decidim_proposals_proposal_votes: {
42
+ decidim_author_id: options[:current_user]
43
+ })
44
+ else
45
+ query
46
+ end
19
47
  end
20
48
 
21
49
  # Returns the random proposals for the current page.
22
- def proposals
50
+ def results
23
51
  @proposals ||= Proposal.transaction do
24
52
  Proposal.connection.execute("SELECT setseed(#{Proposal.connection.quote(random_seed)})")
25
- Proposal.where(feature: feature).reorder("RANDOM()").page(page).per(per_page).load
53
+ super.reorder("RANDOM()").load
26
54
  end
27
55
  end
28
56
 
@@ -8,12 +8,12 @@
8
8
 
9
9
  <% if @form.categories&.any? %>
10
10
  <div class="field">
11
- <%= form.categories_select :category_id, @form.categories, t(".select_a_category") %>
11
+ <%= form.categories_select :category_id, @form.categories, prompt: t(".select_a_category") %>
12
12
  </div>
13
13
  <% end %>
14
14
 
15
15
  <% if @form.scopes&.any? %>
16
16
  <div class="field">
17
- <%= form.select :scope_id, @form.scopes.map{|s| [translated_attribute(s.name), s.id]}, prompt: t(".select_a_scope") %>
17
+ <%= form.select :scope_id, @form.scopes.map{|s| [s.name, s.id]}, prompt: t(".select_a_scope") %>
18
18
  </div>
19
19
  <% end %>
@@ -0,0 +1,5 @@
1
+ var $proposalVotesCount = $('#proposal-<%= @proposal.id %>-votes-count');
2
+ var $proposalVoteButton = $('#proposal-<%= @proposal.id %>-vote-button');
3
+
4
+ $proposalVotesCount.html('<%= j(render partial: 'decidim/proposals/proposals/votes_count', locals: { proposal: @proposal, from_proposals_list: @from_proposals_list }) %>');
5
+ $proposalVoteButton.html('<%= j(render partial: 'decidim/proposals/proposals/vote_button', locals: { proposal: @proposal, from_proposals_list: @from_proposals_list }) %>');
@@ -0,0 +1 @@
1
+ <%= t(".proposals_count", count: @proposals.total_count) %>
@@ -0,0 +1,26 @@
1
+ <%= filter_form_for filter do |form| %>
2
+ <div class="filters__section">
3
+ <div class="filters__search">
4
+ <div class="input-group">
5
+ <%= form.search_field :search_text, label: false, class: "input-group-field", placeholder: t('.search') %>
6
+ <div class="input-group-button">
7
+ <button type="submit" class="button button--muted">
8
+ <%= icon "magnifying-glass", aria_label: t('.search') %>
9
+ </button>
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </div>
14
+
15
+ <%= form.collection_radio_buttons :origin, [["all", t('.all')], ["official", t('.official')], ["citizenship", t('.citizenship')]], :first, :last, legend_title: t('.origin') %>
16
+
17
+ <% if current_user && current_settings.votes_enabled? %>
18
+ <%= form.collection_check_boxes :activity, [["voted", t('.voted')]], :first, :last, legend_title: t('.activity') %>
19
+ <% end %>
20
+
21
+ <% if current_feature.categories.any? %>
22
+ <%= form.categories_select :category_id, current_feature.categories, legend_title: t('.category'), disable_parents: false, label: false, include_blank: true %>
23
+ <% end %>
24
+
25
+ <%= form.hidden_field :random_seed %>
26
+ <% end %>
@@ -26,8 +26,17 @@
26
26
  </div>
27
27
  <div class="card__footer">
28
28
  <div class="card__support">
29
- <div class="card__support__data"></div>
30
- <%= link_to t(".view_proposal"), proposal, class: "card__button button small secondary" %>
29
+ <% if current_settings.votes_enabled? %>
30
+ <div id="proposal-<%= proposal.id %>-votes-count" class="card__support__data">
31
+ <%= render partial: "votes_count", locals: { proposal: proposal, from_proposals_list: true } %>
32
+ </div>
33
+ <div id="proposal-<%= proposal.id %>-vote-button">
34
+ <%= render partial: "vote_button", locals: { proposal: proposal, from_proposals_list: true } %>
35
+ </div>
36
+ <% else %>
37
+ <div class="card__support__data"></div>
38
+ <%= link_to t(".view_proposal"), proposal, class: "card__button button small secondary" %>
39
+ <% end %>
31
40
  </div>
32
41
  </div>
33
42
  </article>
@@ -0,0 +1,4 @@
1
+ <div class="row small-up-1 medium-up-2 card-grid">
2
+ <%= render @proposals %>
3
+ </div>
4
+ <%= decidim_paginate @proposals, random_seed: @random_seed %>
@@ -0,0 +1,33 @@
1
+ <div class="text-center">
2
+ <button class="link text-center" data-open="processShare">
3
+ <%= t(".share") %>
4
+ <%= icon "share", class: "icon--after"%>
5
+ </button>
6
+ </div>
7
+ <div class="reveal" id="processShare" data-reveal>
8
+ <div class="reveal__header">
9
+ <h3 class="reveal__title"><%= t(".share") %>:</h3>
10
+ <button class="close-button" data-close aria-label="<%= t(".close_window") %>" type="button">
11
+ <span aria-hidden="true">&times;</span>
12
+ </button>
13
+ </div>
14
+ <div class="button-group text-center">
15
+ <%= social_share_button_tag(content_for(:meta_title),
16
+ url: content_for(:meta_url),
17
+ image: content_for(:meta_image_url),
18
+ desc: content_for(:meta_description),
19
+ via: content_for(:twitter_handler)) %>
20
+ <a class="button secondary" data-open="urlShare">
21
+ <%= icon "link-intact" %>
22
+ </a>
23
+ </div>
24
+ </div>
25
+ <div class="reveal" id="urlShare" data-reveal>
26
+ <div class="reveal__header">
27
+ <h3 class="reveal__title"><%= t(".share_link") %>:</h3>
28
+ <button class="close-button" data-close aria-label="<%= t(".close_window") %>" type="button">
29
+ <span aria-hidden="true">&times;</span>
30
+ </button>
31
+ </div>
32
+ <h4 class="heading4"><%= "#{content_for(:meta_url)}" %></h4>
33
+ </div>
@@ -0,0 +1,13 @@
1
+ <% if current_user %>
2
+ <% if proposal.voted_by? current_user %>
3
+ <button class="card__button button <%= vote_button_classes(from_proposals_list) %> success">
4
+ <%= t('.already_voted') %>
5
+ </button>
6
+ <% else %>
7
+ <%= button_to t('.vote'), proposal_proposal_votes_path(proposal_id: proposal, from_proposals_list: from_proposals_list), remote: true, data: { disable: true }, class: "card__button button #{vote_button_classes(from_proposals_list)}" %>
8
+ <% end %>
9
+ <% else %>
10
+ <button class="card__button button <%= vote_button_classes(from_proposals_list) %>" data-toggle="loginModal">
11
+ <%= t('.vote') %>
12
+ </button>
13
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <span class="<%= votes_count_classes(from_proposals_list)[:number] %>">
2
+ <%= proposal.votes.size %>
3
+ </span>
4
+ <span class="<%= votes_count_classes(from_proposals_list)[:label] %>">
5
+ <%= t('.count', count: proposal.votes.size) %>
6
+ </span>
@@ -1,6 +1,8 @@
1
1
  <div class="row columns">
2
2
  <div class="title-action">
3
- <h2 class="title-action__title section-heading"><%= t(".proposals_count", count: @proposals.total_count) %></h2>
3
+ <h2 id="proposals-count" class="title-action__title section-heading">
4
+ <%= render partial: "count" %>
5
+ </h2>
4
6
  <%= link_to new_proposal_path, class: "title-action__action button small hollow" do %>
5
7
  <%= t(".new_proposal") %>
6
8
  <%= icon "plus" %>
@@ -8,16 +10,14 @@
8
10
  </div>
9
11
  </div>
10
12
  <div class="row">
11
- <div class="columns mediumlarge-12 large-12">
12
- <div class="row small-up-1 medium-up-3 card-grid">
13
- <%= render @proposals %>
13
+ <div class="columns mediumlarge-4 large-3">
14
+ <div class="card card--secondary show-for-mediumlarge" >
15
+ <%= render partial: "filters" %>
14
16
  </div>
15
- <%
16
- # Kaminari uses url_for to generate the url, but this doesn't play nice with our engine system
17
- # and unless we remove these params they are added again as query string 😩
18
- params.delete("participatory_process_id")
19
- params.delete("feature_id")
20
- %>
21
- <%= paginate @proposals, theme: "decidim", params: { random_seed: @random_seed } %>
17
+ </div>
18
+ <div id="proposals" class="columns mediumlarge-8 large-9">
19
+ <%= render partial: "proposals" %>
22
20
  </div>
23
21
  </div>
22
+
23
+ <%= render partial: "decidim/shared/login_modal" %>
@@ -0,0 +1,5 @@
1
+ var $proposals = $('#proposals');
2
+ var $proposalsCount = $('#proposals-count');
3
+
4
+ $proposals.html('<%= j(render partial: "proposals") %>');
5
+ $proposalsCount.html('<%= j(render partial: "count") %>');
@@ -1,3 +1,8 @@
1
+ <% content_for :meta_description, @proposal.body %>
2
+ <% content_for :meta_title, @proposal.title %>
3
+ <% content_for :meta_url, proposal_url(@proposal.id) %>
4
+ <% content_for :twitter_handler, current_organization.twitter_handler %>
5
+
1
6
  <div class="row column view-header">
2
7
  <h2 class="heading2"><%= @proposal.title %></h2>
3
8
  <div class="author-data">
@@ -16,22 +21,44 @@
16
21
  </div>
17
22
  <div class="row">
18
23
  <div class="columns section view-side mediumlarge-4 mediumlarge-push-8 large-3 large-push-9">
24
+ <% if current_settings.votes_enabled? %>
25
+ <div class="card extra">
26
+ <div class="card__content">
27
+ <div id="proposal-<%= @proposal.id %>-votes-count">
28
+ <%= render partial: "votes_count", locals: { proposal: @proposal, from_proposals_list: false } %>
29
+ </div>
30
+ <div id="proposal-<%= @proposal.id %>-vote-button">
31
+ <%= render partial: "vote_button", locals: { proposal: @proposal, from_proposals_list: false } %>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <% end %>
36
+ <%= render partial: "share", locals: { proposal: @proposal } %>
19
37
  </div>
20
38
  <div class="columns mediumlarge-8 mediumlarge-pull-4">
21
39
  <div class="section">
22
40
  <p><%= @proposal.body %></p>
23
- <% if @proposal.category %>
41
+ <% if @proposal.category || @proposal.scope %>
24
42
  <ul class="tags tags--proposal">
25
- <li><a href=""><%= translated_attribute(@proposal.category.name) %></a></li>
43
+ <% if @proposal.category %>
44
+ <li><a href=""><%= translated_attribute(@proposal.category.name) %></a></li>
45
+ <% end %>
46
+ <% if @proposal.scope %>
47
+ <li><a href=""><%= @proposal.scope.name %></a></li>
48
+ <% end %>
26
49
  </ul>
27
50
  <% end %>
28
51
  </div>
29
52
  </div>
30
53
  </div>
31
54
 
32
-
33
55
  <%= content_for :expanded do %>
34
56
  <% if @proposal.commentable? %>
35
- <%= comments_for @proposal, arguable: true %>
57
+ <%= comments_for @proposal, arguable: true, votable: true %>
36
58
  <% end %>
37
59
  <% end %>
60
+
61
+ <%= render partial: "decidim/shared/login_modal" %>
62
+
63
+ <%= javascript_include_tag "decidim/proposals/social_share" %>
64
+ <%= stylesheet_link_tag "decidim/proposals/social_share" %>
@@ -2,3 +2,4 @@ base_locale: en
2
2
  locales: [en]
3
3
  ignore_unused:
4
4
  - "decidim.features.proposals.name"
5
+ - "decidim.features.proposals.settings.*"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ SocialShareButton.configure do |config|
3
+ config.allow_sites = %w(twitter facebook google_plus)
4
+ end
@@ -3,6 +3,9 @@ ca:
3
3
  features:
4
4
  proposals:
5
5
  name: Propostes
6
+ settings:
7
+ step:
8
+ votes_enabled: Vots habilitats
6
9
  proposals:
7
10
  actions:
8
11
  new: Nova proposta
@@ -33,9 +36,21 @@ ca:
33
36
  scope: Àmbit
34
37
  title: Títol
35
38
  proposals:
39
+ count:
40
+ proposals_count:
41
+ one: 1 proposta
42
+ other: "%{count} propostes"
43
+ filters:
44
+ activity: Activitat
45
+ all: Tots
46
+ category: Categoria
47
+ citizenship: Ciutadania
48
+ official: Oficial
49
+ origin: Origen
50
+ search: Cerca
51
+ voted: Votat
36
52
  index:
37
53
  new_proposal: Nova proposta
38
- proposals_count: "%{count} propostes"
39
54
  new:
40
55
  back: Enrere
41
56
  select_a_category: Si us plau, seleccioni una categoria
@@ -43,4 +58,15 @@ ca:
43
58
  send: Enviar
44
59
  title: Nova proposta
45
60
  proposal:
46
- view_proposal: Veure proposta
61
+ view_proposal: Veure proposta
62
+ share:
63
+ close_window: Tanca la finestra
64
+ share: Compartir
65
+ share_link: Comparteix l'enllaç
66
+ vote_button:
67
+ already_voted: Ja has votat
68
+ vote: Votar
69
+ votes_count:
70
+ count:
71
+ one: VOT
72
+ other: VOTS
@@ -4,6 +4,9 @@ en:
4
4
  features:
5
5
  proposals:
6
6
  name: Proposals
7
+ settings:
8
+ step:
9
+ votes_enabled: Votes enabled
7
10
  proposals:
8
11
  actions:
9
12
  new: New proposal
@@ -34,9 +37,21 @@ en:
34
37
  scope: Scope
35
38
  title: Title
36
39
  proposals:
40
+ count:
41
+ proposals_count:
42
+ one: 1 proposal
43
+ other: "%{count} proposals"
44
+ filters:
45
+ activity: Activity
46
+ all: All
47
+ category: Category
48
+ citizenship: Citizenship
49
+ official: Official
50
+ origin: Origin
51
+ search: Search
52
+ voted: Voted
37
53
  index:
38
54
  new_proposal: New proposal
39
- proposals_count: "%{count} proposals"
40
55
  new:
41
56
  back: Back
42
57
  select_a_category: Please select a category
@@ -45,3 +60,14 @@ en:
45
60
  title: New proposal
46
61
  proposal:
47
62
  view_proposal: View proposal
63
+ share:
64
+ close_window: Close window
65
+ share: Share
66
+ share_link: Share link
67
+ vote_button:
68
+ already_voted: Already voted
69
+ vote: Vote
70
+ votes_count:
71
+ count:
72
+ one: VOTE
73
+ other: VOTES
@@ -3,6 +3,9 @@ es:
3
3
  features:
4
4
  proposals:
5
5
  name: Propuestas
6
+ settings:
7
+ step:
8
+ votes_enabled: Votos habilitados
6
9
  proposals:
7
10
  actions:
8
11
  new: Nueva propuesta
@@ -33,9 +36,21 @@ es:
33
36
  scope: Ámbito
34
37
  title: Título
35
38
  proposals:
39
+ count:
40
+ proposals_count:
41
+ one: 1 propuesta
42
+ other: "%{count} propuestas"
43
+ filters:
44
+ activity: Actividad
45
+ all: Todas
46
+ category: Categoría
47
+ citizenship: Ciudadanía
48
+ official: Oficial
49
+ origin: Origen
50
+ search: Buscar
51
+ voted: Votado
36
52
  index:
37
53
  new_proposal: Nueva propuesta
38
- proposals_count: "%{count} propuestas"
39
54
  new:
40
55
  back: Atrás
41
56
  select_a_category: Por favor, seleccione una categoría
@@ -43,4 +58,15 @@ es:
43
58
  send: Enviar
44
59
  title: Nueva propuesta
45
60
  proposal:
46
- view_proposal: Ver propuesta
61
+ view_proposal: Ver propuesta
62
+ share:
63
+ close_window: Cerrar ventana
64
+ share: Compartir
65
+ share_link: Compartir enlace
66
+ vote_button:
67
+ already_voted: Ya votado
68
+ vote: Votar
69
+ votes_count:
70
+ count:
71
+ one: VOTAR
72
+ other: VOTOS
@@ -0,0 +1,12 @@
1
+ class CreateProposalVotes < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :decidim_proposals_proposal_votes do |t|
4
+ t.references :decidim_proposal, null: false, index: { name: "decidim_proposals_proposal_vote_proposal" }
5
+ t.references :decidim_author, null: false, index: { name: "decidim_proposals_proposal_vote_author" }
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :decidim_proposals_proposal_votes, [:decidim_proposal_id, :decidim_author_id], unique: true, name: "decidim_proposals_proposal_vote_proposal_author_unique"
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ class AddTextSearchIndexes < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_index :decidim_proposals_proposals, :title, name: "decidim_proposals_proposal_title_search"
4
+ add_index :decidim_proposals_proposals, :body, name: "decidim_proposals_proposal_body_search"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddCounterCacheVotesToProposals < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_column :decidim_proposals_proposals, :proposal_votes_count, :integer, null: false, default: 0
4
+ end
5
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "kaminari"
3
+ require "social-share-button"
3
4
 
4
5
  module Decidim
5
6
  module Proposals
@@ -10,9 +11,15 @@ module Decidim
10
11
  isolate_namespace Decidim::Proposals
11
12
 
12
13
  routes do
13
- resources :proposals, only: [:create, :new, :index, :show]
14
+ resources :proposals, only: [:create, :new, :index, :show] do
15
+ resources :proposal_votes, only: [:create]
16
+ end
14
17
  root to: "proposals#index"
15
18
  end
19
+
20
+ initializer "decidim_proposals.assets" do |app|
21
+ app.config.assets.precompile += %w(decidim_proposals_manifest.js decidim_proposals_manifest.css)
22
+ end
16
23
  end
17
24
  end
18
25
  end
@@ -5,13 +5,18 @@ require_dependency "decidim/features/namer"
5
5
  Decidim.register_feature(:proposals) do |feature|
6
6
  feature.engine = Decidim::Proposals::Engine
7
7
  feature.admin_engine = Decidim::Proposals::AdminEngine
8
+ feature.icon = "decidim/proposals/icon.svg"
8
9
 
9
- feature.on(:destroy) do |instance|
10
+ feature.on(:before_destroy) do |instance|
10
11
  if Decidim::Proposals::Proposal.where(feature: instance).any?
11
12
  raise "Can't destroy this feature when there are proposals"
12
13
  end
13
14
  end
14
15
 
16
+ feature.settings(:step) do |settings|
17
+ settings.attribute :votes_enabled, type: :boolean
18
+ end
19
+
15
20
  feature.seeds do
16
21
  Decidim::ParticipatoryProcess.all.each do |process|
17
22
  next unless process.steps.any?
@@ -19,16 +24,37 @@ Decidim.register_feature(:proposals) do |feature|
19
24
  feature = Decidim::Feature.create!(
20
25
  name: Decidim::Features::Namer.new(process.organization.available_locales, :proposals).i18n_name,
21
26
  manifest_name: :proposals,
22
- participatory_process: process
27
+ participatory_process: process,
28
+ step_settings: {
29
+ process.active_step.id => { votes_enabled: true }
30
+ }
23
31
  )
24
32
 
25
- 20.times do
26
- Decidim::Proposals::Proposal.create!(
33
+ 20.times do |n|
34
+ proposal = Decidim::Proposals::Proposal.create!(
27
35
  feature: feature,
28
36
  title: Faker::Lorem.sentence(2),
29
37
  body: Faker::Lorem.paragraphs(2).join("\n"),
30
38
  author: Decidim::User.where(organization: feature.organization).all.sample
31
39
  )
40
+
41
+ rand(3).times do |m|
42
+ email = "vote-author-#{process.id}-#{n}-#{m}@decidim.org"
43
+ name = "#{Faker::Name.name} #{process.id} #{n} #{m}"
44
+
45
+ author = Decidim::User.create!(email: email,
46
+ password: "password1234",
47
+ password_confirmation: "password1234",
48
+ name: name,
49
+ organization: feature.organization,
50
+ tos_agreement: "1",
51
+ confirmed_at: Time.now)
52
+
53
+ Decidim::Proposals::ProposalVote.create!(proposal: proposal,
54
+ author: author)
55
+ end
56
+
57
+ Decidim::Comments::Seed.comments_for(proposal)
32
58
  end
33
59
  end
34
60
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decidim-proposals
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep Jaume Rey Peroy
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2016-12-21 00:00:00.000000000 Z
13
+ date: 2017-01-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: decidim-core
@@ -18,28 +18,28 @@ dependencies:
18
18
  requirements:
19
19
  - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 0.0.1
21
+ version: 0.0.2
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - '='
27
27
  - !ruby/object:Gem::Version
28
- version: 0.0.1
28
+ version: 0.0.2
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: decidim-comments
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
33
  - - '='
34
34
  - !ruby/object:Gem::Version
35
- version: 0.0.1
35
+ version: 0.0.2
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - '='
41
41
  - !ruby/object:Gem::Version
42
- version: 0.0.1
42
+ version: 0.0.2
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: rectify
45
45
  requirement: !ruby/object:Gem::Requirement
@@ -60,28 +60,42 @@ dependencies:
60
60
  requirements:
61
61
  - - "~>"
62
62
  - !ruby/object:Gem::Version
63
- version: 1.0.0.rc1
63
+ version: 1.0.1
64
64
  type: :runtime
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - "~>"
69
69
  - !ruby/object:Gem::Version
70
- version: 1.0.0.rc1
70
+ version: 1.0.1
71
+ - !ruby/object:Gem::Dependency
72
+ name: social-share-button
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: 0.8.8
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: 0.8.8
71
85
  - !ruby/object:Gem::Dependency
72
86
  name: decidim-dev
73
87
  requirement: !ruby/object:Gem::Requirement
74
88
  requirements:
75
89
  - - '='
76
90
  - !ruby/object:Gem::Version
77
- version: 0.0.1
91
+ version: 0.0.2
78
92
  type: :development
79
93
  prerelease: false
80
94
  version_requirements: !ruby/object:Gem::Requirement
81
95
  requirements:
82
96
  - - '='
83
97
  - !ruby/object:Gem::Version
84
- version: 0.0.1
98
+ version: 0.0.2
85
99
  description: A proposals component for decidim's participatory processes.
86
100
  email:
87
101
  - josepjaume@gmail.com
@@ -93,30 +107,50 @@ extra_rdoc_files: []
93
107
  files:
94
108
  - README.md
95
109
  - Rakefile
110
+ - app/assets/config/decidim_proposals_manifest.css
111
+ - app/assets/config/decidim_proposals_manifest.js
112
+ - app/assets/images/decidim/proposals/icon.svg
113
+ - app/assets/javascripts/decidim/proposals/social_share.js
114
+ - app/assets/stylesheets/decidim/proposals/social_share.css.scss
96
115
  - app/commands/decidim/proposals/admin/create_proposal.rb
97
116
  - app/commands/decidim/proposals/create_proposal.rb
98
117
  - app/controllers/decidim/proposals/admin/application_controller.rb
99
118
  - app/controllers/decidim/proposals/admin/proposals_controller.rb
100
119
  - app/controllers/decidim/proposals/application_controller.rb
120
+ - app/controllers/decidim/proposals/proposal_votes_controller.rb
101
121
  - app/controllers/decidim/proposals/proposals_controller.rb
102
122
  - app/forms/decidim/proposals/admin/proposal_form.rb
103
123
  - app/forms/decidim/proposals/proposal_form.rb
104
124
  - app/helpers/decidim/proposals/application_helper.rb
125
+ - app/helpers/decidim/proposals/proposal_votes_helper.rb
105
126
  - app/models/decidim/proposals/application_record.rb
106
127
  - app/models/decidim/proposals/proposal.rb
128
+ - app/models/decidim/proposals/proposal_vote.rb
107
129
  - app/services/decidim/proposals/proposal_search.rb
108
130
  - app/views/decidim/proposals/admin/proposals/_form.html.erb
109
131
  - app/views/decidim/proposals/admin/proposals/index.html.erb
110
132
  - app/views/decidim/proposals/admin/proposals/new.html.erb
133
+ - app/views/decidim/proposals/proposal_votes/create.js.erb
134
+ - app/views/decidim/proposals/proposals/_count.html.erb
135
+ - app/views/decidim/proposals/proposals/_filters.html.erb
111
136
  - app/views/decidim/proposals/proposals/_proposal.html.erb
137
+ - app/views/decidim/proposals/proposals/_proposals.html.erb
138
+ - app/views/decidim/proposals/proposals/_share.html.erb
139
+ - app/views/decidim/proposals/proposals/_vote_button.html.erb
140
+ - app/views/decidim/proposals/proposals/_votes_count.html.erb
112
141
  - app/views/decidim/proposals/proposals/index.html.erb
142
+ - app/views/decidim/proposals/proposals/index.js.erb
113
143
  - app/views/decidim/proposals/proposals/new.html.erb
114
144
  - app/views/decidim/proposals/proposals/show.html.erb
115
145
  - config/i18n-tasks.yml
146
+ - config/initializers/social_share_button.rb
116
147
  - config/locales/ca.yml
117
148
  - config/locales/en.yml
118
149
  - config/locales/es.yml
119
150
  - db/migrate/20161212110850_create_decidim_proposals.rb
151
+ - db/migrate/20170112115253_create_proposal_votes.rb
152
+ - db/migrate/20170113114245_add_text_search_indexes.rb
153
+ - db/migrate/20170118120151_add_counter_cache_votes_to_proposals.rb
120
154
  - lib/decidim/proposals.rb
121
155
  - lib/decidim/proposals/admin.rb
122
156
  - lib/decidim/proposals/admin_engine.rb
@@ -142,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
176
  version: '0'
143
177
  requirements: []
144
178
  rubyforge_project:
145
- rubygems_version: 2.5.2
179
+ rubygems_version: 2.6.8
146
180
  signing_key:
147
181
  specification_version: 4
148
182
  summary: A proposals component for decidim's participatory processes.