decidim-proposals 0.0.1 → 0.0.2

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.
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.