Show a preview list of investments in the budget landing page
Note one of the tests dealing with random results is a bit flaky; since it's a permutation selecting 7 objects out of 12, it will fail about once every 4 million times. We think this is acceptable. Co-Authored-By: Julian Nicolas Herrero <microweb10@gmail.com>
This commit is contained in:
BIN
app/assets/images/budget_investment_no_image.jpg
Normal file
BIN
app/assets/images/budget_investment_no_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
78
app/assets/stylesheets/budgets/investments-list.scss
Normal file
78
app/assets/stylesheets/budgets/investments-list.scss
Normal file
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,3 +37,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render Budgets::InvestmentsListComponent.new(budget) %>
|
||||
|
||||
33
app/components/budgets/investment_component.html.erb
Normal file
33
app/components/budgets/investment_component.html.erb
Normal file
@@ -0,0 +1,33 @@
|
||||
<div id="<%= dom_id(investment) %>" class="investments-list-item">
|
||||
<% 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? %>
|
||||
<div class="supports-and-price">
|
||||
<% if investment.should_show_vote_count? %>
|
||||
<span class="supports-title"><%= t("budgets.investments.investment.support_title") %></span>
|
||||
<strong><%= investment.total_votes %></strong>
|
||||
<% end %>
|
||||
|
||||
<% if investment.should_show_price? %>
|
||||
<span class="price-title"><%= t("budgets.investments.investment.price_title") %></span>
|
||||
<strong><%= investment.formatted_price %></strong>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="budget-investment-content">
|
||||
<% cache [locale_and_user_status(investment), "index", investment, investment.author] do %>
|
||||
<h3><%= link_to investment.title, namespaced_budget_investment_path(investment) %></h3>
|
||||
|
||||
<%= render Budgets::Investments::InfoComponent.new(investment) %>
|
||||
|
||||
<%= link_to namespaced_budget_investment_path(investment), class: "read-more" do %>
|
||||
<small><%= t("budgets.investments.index.read_more") %></small>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
8
app/components/budgets/investment_component.rb
Normal file
8
app/components/budgets/investment_component.rb
Normal file
@@ -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
|
||||
21
app/components/budgets/investments_list_component.html.erb
Normal file
21
app/components/budgets/investments_list_component.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<% if investments.any? %>
|
||||
<section class="investments-list">
|
||||
<header>
|
||||
<h2><%= t("budgets.show.investments_list") %></h2>
|
||||
</header>
|
||||
|
||||
<% investments.each do |investment| %>
|
||||
<%= render Budgets::InvestmentComponent.new(investment) %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<% unless budget.informing? %>
|
||||
<div class="row margin-top">
|
||||
<div class="small-12 medium-6 large-4 small-centered column margin-top">
|
||||
<%= link_to t("budgets.show.see_all_investments"),
|
||||
budget_investments_path(budget),
|
||||
class: "button expanded" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
24
app/components/budgets/investments_list_component.rb
Normal file
24
app/components/budgets/investments_list_component.rb
Normal file
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
59
spec/components/budgets/investment_component_spec.rb
Normal file
59
spec/components/budgets/investment_component_spec.rb
Normal file
@@ -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
|
||||
63
spec/components/budgets/investments_list_component_spec.rb
Normal file
63
spec/components/budgets/investments_list_component_spec.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user