diff --git a/app/assets/images/budget_investment_no_image.jpg b/app/assets/images/budget_investment_no_image.jpg
new file mode 100644
index 000000000..7de0e0a2c
Binary files /dev/null and b/app/assets/images/budget_investment_no_image.jpg differ
diff --git a/app/assets/stylesheets/budgets/investments-list.scss b/app/assets/stylesheets/budgets/investments-list.scss
new file mode 100644
index 000000000..a5d8b8973
--- /dev/null
+++ b/app/assets/stylesheets/budgets/investments-list.scss
@@ -0,0 +1,78 @@
+.investments-list {
+ @include flex-grid-row;
+ margin-top: $line-height;
+
+ > header {
+ @include grid-column-gutter;
+ width: 100%;
+ }
+
+ .investments-list-item {
+ @extend %panel;
+ @include xy-gutters;
+ border-radius: rem-calc(6);
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ margin-bottom: $line-height / 2;
+ position: relative;
+ width: 100%;
+
+ @include breakpoint(medium) {
+ $gap: rem-calc(map-get($grid-margin-gutters, medium));
+ width: calc(33.33% - #{$gap});
+ }
+
+ img {
+ border-top-left-radius: rem-calc(6);
+ border-top-right-radius: rem-calc(6);
+ height: $line-height * 10;
+ width: fit-content;
+ object-fit: cover;
+
+ @include breakpoint(medium only) {
+ height: $line-height * 10;
+ }
+ }
+
+ .budget-investment-content {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ padding: 0 rem-calc(12) rem-calc(6);
+ min-height: $line-height * 8;
+
+ .investment-project-info {
+ flex: 1;
+ margin-bottom: $line-height * 2;
+ }
+ }
+
+ .read-more {
+ align-self: flex-end;
+ }
+ }
+
+ .supports-and-price {
+ @include has-fa-icon(award, solid, after);
+ background: $light;
+ display: flex;
+ flex-wrap: wrap;
+ padding: rem-calc(6) rem-calc(12);
+
+ .price-title,
+ .supports-title {
+ color: #7b7b7b;
+ font-size: $small-font-size;
+ text-transform: uppercase;
+ width: 100%;
+ }
+
+ &::after {
+ font-size: 2em;
+ margin-left: auto;
+ margin-right: 0;
+ transform: translateY(-25%);
+ }
+ }
+}
diff --git a/app/components/budgets/budget_component.html.erb b/app/components/budgets/budget_component.html.erb
index b7a77989f..f87ff306c 100644
--- a/app/components/budgets/budget_component.html.erb
+++ b/app/components/budgets/budget_component.html.erb
@@ -37,3 +37,5 @@
+
+<%= render Budgets::InvestmentsListComponent.new(budget) %>
diff --git a/app/components/budgets/investment_component.html.erb b/app/components/budgets/investment_component.html.erb
new file mode 100644
index 000000000..5b0785aa1
--- /dev/null
+++ b/app/components/budgets/investment_component.html.erb
@@ -0,0 +1,33 @@
+
+ <% if investment.image.present? %>
+ <%= image_tag investment.image_url(:large), alt: investment.image.title.unicode_normalize %>
+ <% else %>
+ <%= image_tag "budget_investment_no_image.jpg", alt: investment.title %>
+ <% end %>
+
+ <% if investment.should_show_vote_count? || investment.should_show_price? %>
+
+ <% if investment.should_show_vote_count? %>
+ <%= t("budgets.investments.investment.support_title") %>
+ <%= investment.total_votes %>
+ <% end %>
+
+ <% if investment.should_show_price? %>
+ <%= t("budgets.investments.investment.price_title") %>
+ <%= investment.formatted_price %>
+ <% end %>
+
+ <% end %>
+
+
+ <% cache [locale_and_user_status(investment), "index", investment, investment.author] do %>
+
<%= link_to investment.title, namespaced_budget_investment_path(investment) %>
+
+ <%= render Budgets::Investments::InfoComponent.new(investment) %>
+
+ <%= link_to namespaced_budget_investment_path(investment), class: "read-more" do %>
+ <%= t("budgets.investments.index.read_more") %>
+ <% end %>
+ <% end %>
+
+
diff --git a/app/components/budgets/investment_component.rb b/app/components/budgets/investment_component.rb
new file mode 100644
index 000000000..e52af44d9
--- /dev/null
+++ b/app/components/budgets/investment_component.rb
@@ -0,0 +1,8 @@
+class Budgets::InvestmentComponent < ApplicationComponent
+ delegate :locale_and_user_status, :namespaced_budget_investment_path, to: :helpers
+ attr_reader :investment
+
+ def initialize(investment)
+ @investment = investment
+ end
+end
diff --git a/app/components/budgets/investments_list_component.html.erb b/app/components/budgets/investments_list_component.html.erb
new file mode 100644
index 000000000..6e39b28ab
--- /dev/null
+++ b/app/components/budgets/investments_list_component.html.erb
@@ -0,0 +1,21 @@
+<% if investments.any? %>
+
+
+ <%= t("budgets.show.investments_list") %>
+
+
+ <% investments.each do |investment| %>
+ <%= render Budgets::InvestmentComponent.new(investment) %>
+ <% end %>
+
+<% end %>
+
+<% unless budget.informing? %>
+
+
+ <%= link_to t("budgets.show.see_all_investments"),
+ budget_investments_path(budget),
+ class: "button expanded" %>
+
+
+<% end %>
diff --git a/app/components/budgets/investments_list_component.rb b/app/components/budgets/investments_list_component.rb
new file mode 100644
index 000000000..ab2a6b80d
--- /dev/null
+++ b/app/components/budgets/investments_list_component.rb
@@ -0,0 +1,24 @@
+class Budgets::InvestmentsListComponent < ApplicationComponent
+ attr_reader :budget
+
+ def initialize(budget)
+ @budget = budget
+ end
+
+ def render?
+ budget.single_heading?
+ end
+
+ def investments(limit: 9)
+ case budget.phase
+ when "accepting", "reviewing"
+ budget.investments.sample(limit)
+ when "selecting", "valuating", "publishing_prices"
+ budget.investments.feasible.sample(limit)
+ when "balloting", "reviewing_ballots"
+ budget.investments.selected.sample(limit)
+ else
+ budget.investments.none
+ end
+ end
+end
diff --git a/config/locales/en/budgets.yml b/config/locales/en/budgets.yml
index 8b5193a76..b433d2749 100644
--- a/config/locales/en/budgets.yml
+++ b/config/locales/en/budgets.yml
@@ -130,6 +130,7 @@ en:
random: random
confidence_score: highest rated
price: by price
+ read_more: "Read more"
share:
message: "I created the investment project %{title} in %{handle}. Create an investment project too!"
show:
@@ -161,6 +162,8 @@ en:
confirm_group:
one: "You can only support investments in %{count} district. If you continue you cannot change the election of your district. Are you sure?"
other: "You can only support investments in %{count} districts. If you continue you cannot change the election of your district. Are you sure?"
+ price_title: "Price"
+ support_title: "Supports"
supports:
one: 1 support
other: "%{count} supports"
@@ -174,6 +177,8 @@ en:
price: "Total budget"
show:
see_results: See results
+ see_all_investments: "See all investments"
+ investments_list: "List of investments"
results:
link: Results
page_title: "%{budget} - Results"
diff --git a/config/locales/es/budgets.yml b/config/locales/es/budgets.yml
index 8ec55b7e1..8b4dd9d3d 100644
--- a/config/locales/es/budgets.yml
+++ b/config/locales/es/budgets.yml
@@ -130,6 +130,7 @@ es:
random: Aleatorios
confidence_score: Mejor valorados
price: Por coste
+ read_more: "Leer más"
share:
message: "He presentado el proyecto %{title} en %{handle}. ¡Presenta un proyecto tú también!"
show:
@@ -161,6 +162,8 @@ es:
confirm_group:
one: "Sólo puedes apoyar proyectos en %{count} distrito. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
other: "Sólo puedes apoyar proyectos en %{count} distritos. Si sigues adelante no podrás cambiar la elección de este distrito. ¿Estás seguro/a?"
+ price_title: "Coste"
+ support_title: "Apoyos"
supports:
zero: Sin apoyos
one: 1 apoyo
@@ -174,6 +177,8 @@ es:
price: "Presupuesto total"
show:
see_results: Ver resultados
+ see_all_investments: "Ver todos los proyectos"
+ investments_list: "Lista de proyectos"
results:
link: Resultados
page_title: "%{budget} - Resultados"
diff --git a/spec/components/budgets/investment_component_spec.rb b/spec/components/budgets/investment_component_spec.rb
new file mode 100644
index 000000000..0edb520ed
--- /dev/null
+++ b/spec/components/budgets/investment_component_spec.rb
@@ -0,0 +1,59 @@
+require "rails_helper"
+
+describe Budgets::InvestmentComponent, type: :component do
+ let(:user) { create(:user) }
+
+ before do
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ it "shows the investment image when defined" do
+ investment = create(:budget_investment, :with_image)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).not_to have_css "img[src*='budget_investment_no_image']"
+ expect(page).to have_css "img[alt='#{investment.image.title}']"
+ end
+
+ it "shows the default image when investment has not an image defined" do
+ investment = create(:budget_investment)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).to have_css "img[src*='budget_investment_no_image']"
+ end
+
+ it "shows supports count when budget is valuating" do
+ budget = create(:budget, :valuating)
+ investment = create(:budget_investment, budget: budget)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).to have_content "Supports"
+
+ budget.update!(phase: (Budget::Phase::PHASE_KINDS - ["valuating"]).sample)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).not_to have_content "Supports"
+ end
+
+ it "shows price when investment is selected and budget prices are published" do
+ budget = create(:budget, :finished)
+ investment = create(:budget_investment, :selected, budget: budget)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).to have_content "Price"
+
+ budget.update!(phase: (Budget::Phase::PHASE_KINDS - Budget::Phase::PUBLISHED_PRICES_PHASES).sample)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).not_to have_content "Price"
+ end
+
+ it "shows investment information" do
+ investment = create(:budget_investment)
+ render_inline Budgets::InvestmentComponent.new(investment)
+
+ expect(page).to have_link investment.title
+ expect(page).to have_link "Read more"
+ expect(page).to have_css ".investment-project-info"
+ end
+end
diff --git a/spec/components/budgets/investments_list_component_spec.rb b/spec/components/budgets/investments_list_component_spec.rb
new file mode 100644
index 000000000..ca1864518
--- /dev/null
+++ b/spec/components/budgets/investments_list_component_spec.rb
@@ -0,0 +1,63 @@
+require "rails_helper"
+
+describe Budgets::InvestmentsListComponent, type: :component do
+ describe "#investments_preview_list" do
+ let(:budget) { create(:budget, :accepting) }
+ let(:group) { create(:budget_group, budget: budget) }
+ let(:heading) { create(:budget_heading, group: group) }
+ let(:component) { Budgets::InvestmentsListComponent.new(budget) }
+
+ let!(:normal_investments) { create_list(:budget_investment, 4, heading: heading) }
+ let!(:feasible_investments) { create_list(:budget_investment, 4, :feasible, heading: heading) }
+ let!(:selected_investments) { create_list(:budget_investment, 4, :selected, heading: heading) }
+
+ it "returns an empty relation if phase is informing or finished" do
+ %w[informing finished].each do |phase_name|
+ budget.phase = phase_name
+
+ expect(component.investments).to be_empty
+ end
+ end
+
+ it "returns a maximum 9 investments by default" do
+ expect(budget.investments.count).to be 12
+ expect(component.investments.count).to be 9
+ end
+
+ it "returns a different random array of investments every time" do
+ expect(component.investments(limit: 7)).not_to eq component.investments(limit: 7)
+ end
+
+ it "returns any investments while accepting and reviewing" do
+ %w[accepting reviewing].each do |phase_name|
+ budget.phase = phase_name
+
+ investments = component.investments(limit: 9)
+
+ expect(investments & normal_investments).not_to be_empty
+ expect(investments & feasible_investments).not_to be_empty
+ expect(investments & selected_investments).not_to be_empty
+ end
+ end
+
+ it "returns only feasible investments if phase is selecting, valuating or publishing_prices" do
+ %w[selecting valuating publishing_prices].each do |phase_name|
+ budget.phase = phase_name
+
+ investments = component.investments(limit: 6)
+
+ expect(feasible_investments + selected_investments).to include(*investments)
+ end
+ end
+
+ it "returns only selected investments if phase is balloting or reviewing_ballots" do
+ %w[balloting reviewing_ballots].each do |phase_name|
+ budget.phase = phase_name
+
+ investments = component.investments(limit: 3)
+
+ expect(selected_investments).to include(*investments)
+ end
+ end
+ end
+end
diff --git a/spec/system/budgets/budgets_spec.rb b/spec/system/budgets/budgets_spec.rb
index e851c0c19..5d9a0c210 100644
--- a/spec/system/budgets/budgets_spec.rb
+++ b/spec/system/budgets/budgets_spec.rb
@@ -270,6 +270,123 @@ describe "Budgets" do
expect(page).to have_link "See results"
end
+
+ scenario "Show link to see all investments" do
+ budget = create(:budget)
+ group = create(:budget_group, budget: budget)
+ heading = create(:budget_heading, group: group)
+
+ create_list(:budget_investment, 3, :selected, heading: heading, price: 999)
+
+ budget.update!(phase: "informing")
+
+ visit budget_path(budget)
+ expect(page).not_to have_link "See all investments"
+
+ %w[accepting reviewing selecting valuating].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+ expect(page).to have_link "See all investments",
+ href: budget_investments_path(budget)
+ end
+
+ %w[publishing_prices balloting reviewing_ballots].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+ expect(page).to have_link "See all investments",
+ href: budget_investments_path(budget)
+ end
+
+ budget.update!(phase: "finished")
+
+ visit budget_path(budget)
+ expect(page).to have_link "See all investments",
+ href: budget_investments_path(budget)
+ end
+
+ scenario "Show investments list" do
+ budget = create(:budget)
+ group = create(:budget_group, budget: budget)
+ heading = create(:budget_heading, group: group)
+
+ create_list(:budget_investment, 3, :selected, heading: heading, price: 999)
+
+ %w[informing finished].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+
+ expect(page).not_to have_content "List of investments"
+ expect(page).not_to have_css ".investments-list"
+ expect(page).not_to have_css ".budget-investment"
+ end
+
+ %w[accepting reviewing selecting].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+
+ within(".investments-list") do
+ expect(page).to have_content "List of investments"
+ expect(page).not_to have_content "SUPPORTS"
+ expect(page).not_to have_content "PRICE"
+ end
+ end
+
+ budget.update!(phase: "valuating")
+
+ visit budget_path(budget)
+
+ within(".investments-list") do
+ expect(page).to have_content "List of investments"
+ expect(page).to have_content("SUPPORTS", count: 3)
+ expect(page).not_to have_content "PRICE"
+ end
+
+ %w[publishing_prices balloting reviewing_ballots].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+
+ within(".investments-list") do
+ expect(page).to have_content "List of investments"
+ expect(page).to have_content("PRICE", count: 3)
+ end
+ end
+ end
+
+ scenario "Do not show investments list when budget has multiple headings" do
+ budget = create(:budget)
+ group = create(:budget_group, budget: budget)
+ heading_1 = create(:budget_heading, group: group)
+ create(:budget_heading, group: group)
+
+ create_list(:budget_investment, 3, :selected, heading: heading_1, price: 999)
+
+ %w[accepting reviewing selecting].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+
+ expect(page).not_to have_css ".investments-list"
+ end
+
+ budget.update!(phase: "valuating")
+
+ visit budget_path(budget)
+
+ expect(page).not_to have_css ".investments-list"
+
+ %w[publishing_prices balloting reviewing_ballots].each do |phase_name|
+ budget.update!(phase: phase_name)
+
+ visit budget_path(budget)
+
+ expect(page).not_to have_css ".investments-list"
+ end
+ end
end
context "In Drafting phase" do
diff --git a/spec/system/budgets/investments_spec.rb b/spec/system/budgets/investments_spec.rb
index c73bdf3ba..aedf5893c 100644
--- a/spec/system/budgets/investments_spec.rb
+++ b/spec/system/budgets/investments_spec.rb
@@ -54,7 +54,7 @@ describe "Budget Investments" do
unfeasible_investment = create(:budget_investment, :unfeasible, heading: heading)
visit budget_path(budget)
- click_link "More hospitals"
+ click_link "See all investments"
expect(page).to have_selector("#budget-investments .budget-investment", count: 3)
investments.each do |investment|
@@ -1245,7 +1245,7 @@ describe "Budget Investments" do
first(:link, "Participatory budgeting").click
- click_link "More hospitals"
+ click_link "See all investments"
within("#budget_investment_#{investment1.id}") do
expect(page).to have_content investment1.title