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