Merge pull request #2864 from consul/backport_1543-budget_execution_list

Budget execution list
This commit is contained in:
Javier Martín
2018-11-07 15:44:57 +01:00
committed by GitHub
20 changed files with 537 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -1680,6 +1680,59 @@
}
}
.budget-execution {
border: 1px solid $border;
overflow: hidden;
position: relative;
a {
color: $text;
display: block;
img {
height: $line-height * 9;
transition-duration: 0.3s;
transition-property: transform;
width: 100%;
}
&:hover {
text-decoration: none;
img {
transform: scale(1.05);
}
}
}
h5 {
font-size: $base-font-size;
margin-bottom: 0;
}
.budget-execution-info {
padding: $line-height / 2;
}
.author {
color: $text-medium;
font-size: $small-font-size;
}
.budget-execution-content {
min-height: $line-height * 3;
}
.price {
color: $budget;
font-size: rem-calc(24);
}
&:hover {
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.2);
}
}
// 07. Proposals successful
// -------------------------

View File

@@ -0,0 +1,46 @@
module Budgets
class ExecutionsController < ApplicationController
before_action :load_budget
load_and_authorize_resource :budget
def show
authorize! :read_executions, @budget
@statuses = ::Budget::Investment::Status.all
if params[:status].present?
@investments_by_heading = @budget.investments.winners
.joins(:milestones).includes(:milestones)
.select { |i| i.milestones.published.with_status
.order_by_publication_date.last
.status_id == params[:status].to_i }
.uniq
.group_by(&:heading)
else
@investments_by_heading = @budget.investments.winners
.joins(:milestones).includes(:milestones)
.distinct.group_by(&:heading)
end
@investments_by_heading = reorder_alphabetically_with_city_heading_first.to_h
end
private
def load_budget
@budget = Budget.find_by(slug: params[:id]) || Budget.find_by(id: params[:id])
end
def reorder_alphabetically_with_city_heading_first
@investments_by_heading.sort do |a, b|
if a[0].name == 'Toda la ciudad'
-1
elsif b[0].name == 'Toda la ciudad'
1
else
a[0].name <=> b[0].name
end
end
end
end
end

View File

@@ -0,0 +1,9 @@
module BudgetExecutionsHelper
def filters_select_counts(status)
@budget.investments.winners.with_milestones.select { |i| i.milestones
.published.with_status.order_by_publication_date
.last.status_id == status rescue false }.count
end
end

View File

@@ -22,7 +22,7 @@ module Abilities
can [:read], Budget
can [:read], Budget::Group
can [:read, :print, :json_data], Budget::Investment
can :read_results, Budget, phase: "finished"
can [:read_results, :read_executions], Budget, phase: "finished"
can :new, DirectMessage
can [:read, :debate, :draft_publication, :allegations, :result_publication, :proposals], Legislation::Process, published: true
can [:read, :changes, :go_to_version], Legislation::DraftVersion

View File

@@ -84,6 +84,7 @@ class Budget
scope :last_week, -> { where("created_at >= ?", 7.days.ago)}
scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) }
scope :sort_by_created_at, -> { reorder(created_at: :desc) }
scope :with_milestones, -> { joins(:milestones).distinct }
scope :by_budget, ->(budget) { where(budget: budget) }
scope :by_group, ->(group_id) { where(group_id: group_id) }

View File

@@ -18,6 +18,8 @@ class Budget
validate :description_or_status_present?
scope :order_by_publication_date, -> { order(publication_date: :asc) }
scope :published, -> { where("publication_date <= ?", Date.current) }
scope :with_status, -> { where("status_id IS NOT NULL") }
def self.title_max_length
80

View File

@@ -4,7 +4,8 @@ class SiteCustomization::Image < ActiveRecord::Base
"logo_header" => [260, 80],
"social_media_icon" => [470, 246],
"social_media_icon_twitter" => [246, 246],
"apple-touch-icon-200" => [200, 200]
"apple-touch-icon-200" => [200, 200],
"budget_execution_no_image" => [800, 600]
}
has_attached_file :image

View File

@@ -0,0 +1,33 @@
<% @investments_by_heading.each do |heading, investments| %>
<h4 id="<%= heading.name.parameterize %>">
<%= heading.name %> (<%= investments.count %>)
</h4>
<div class="row" data-equalizer-on="medium" data-equalizer>
<% investments.each do |investment| %>
<div class="small-12 medium-6 large-4 column end margin-bottom">
<div class="budget-execution">
<%= link_to budget_investment_path(@budget, investment, anchor: "tab-milestones"), data: { 'equalizer-watch': true } do %>
<% investment.milestones.order(publication_date: :desc).limit(1).each do |milestone| %>
<% if milestone.image.present? %>
<%= image_tag milestone.image_url(:large), alt: milestone.image.title %>
<% elsif investment.image.present? %>
<%= image_tag investment.image_url(:thumb), alt: investment.image.title %>
<% else %>
<%= image_tag "budget_execution_no_image.jpg", alt: investment.title %>
<% end %>
<% end %>
<div class="budget-execution-info">
<div class="budget-execution-content">
<h5><%= investment.title %></h5>
<span class="author"><%= investment.author.name %></span>
</div>
<p class="price margin-top text-center">
<strong><%= investment.formatted_price %></strong>
</p>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<% end %>

View File

@@ -0,0 +1,77 @@
<% provide :title, t("budgets.executions.page_title", budget: @budget.name) %>
<% content_for :meta_description do %><%= @budget.description_for_phase('finished') %><% end %>
<% provide :social_media_meta_tags do %>
<%= render 'shared/social_media_meta_tags',
social_url: budget_executions_url(@budget),
social_title: @budget.name,
social_description: @budget.description_for_phase('finished') %>
<% end %>
<% content_for :canonical do %>
<%= render 'shared/canonical', href: budget_executions_url(@budget) %>
<% end %>
<div class="budgets-stats">
<div class="expanded no-margin-top padding header">
<div class="row">
<div class="small-12 column">
<%= back_link_to budgets_path %>
<h2 class="margin-top">
<%= t("budgets.executions.heading") %><br>
<span><%= @budget.name %></span>
</h2>
</div>
</div>
</div>
</div>
<div class="row margin-top">
<div class="small-12 column">
<ul class="tabs">
<li class="tabs-title">
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
</li>
<li class="tabs-title is-active">
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget), class: 'is-active' %>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="small-12 medium-3 large-2 column">
<h3 class="margin-bottom">
<%= t("budgets.executions.heading_selection_title") %>
</h3>
<ul class="menu vertical no-margin-top no-padding-top">
<% @investments_by_heading.each_pair do |heading, investments| %>
<li>
<%= link_to heading.name, "#" + heading.name.parameterize %>
</li>
<% end %>
</ul>
</div>
<div class="small-12 medium-9 large-10 column">
<%= form_tag(budget_executions_path(@budget), method: :get) do %>
<div class="small-12 medium-3 column">
<%= label_tag t("budgets.executions.filters.label") %>
<%= select_tag :status,
options_from_collection_for_select(@statuses,
:id, lambda { |s| "#{s.name} (#{filters_select_counts(s.id)})" },
params[:status]),
class: "js-submit-on-change",
prompt: t("budgets.executions.filters.all",
count: @budget.investments.winners.with_milestones.count) %>
</div>
<% end %>
<% if @investments_by_heading.any? %>
<%= render 'budgets/executions/investments' %>
<% else %>
<div class="callout primary clear">
<%= t("budgets.executions.no_winner_investments") %>
</div>
<% end %>
</div>
</div>

View File

@@ -33,7 +33,7 @@
<% if current_user %>
<% if current_user.level_two_or_three_verified? %>
<%= link_to t("budgets.investments.index.sidebar.create"),
new_budget_investment_path(@budget),
new_budget_investment_path(current_budget),
class: "button margin-top expanded" %>
<% else %>
<div class="callout warning margin-top">

View File

@@ -29,10 +29,10 @@
<ul class="tabs">
<li class="tabs-title is-active">
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
<%= link_to t("budgets.results.link"), budget_results_path(@budget), class: "is-active" %>
<%= link_to t("budgets.results.link"), budget_results_path(@budget), class: "is-active" %>
</li>
<li class="tabs-title">
<%# link_to t("budgets.stats.link"), budget_stats_path(@budget)%>
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
</li>
</ul>
</div>

View File

@@ -174,6 +174,15 @@ en:
investment_proyects: List of all investment projects
unfeasible_investment_proyects: List of all unfeasible investment projects
not_selected_investment_proyects: List of all investment projects not selected for balloting
executions:
link: "Milestones"
page_title: "%{budget} - Milestones"
heading: "Participatory budget Milestones"
heading_selection_title: "By district"
no_winner_investments: "No winner investments in this state"
filters:
label: "Project's current state"
all: "All (%{count})"
phases:
errors:
dates_range_invalid: "Start date can't be equal or later than End date"

View File

@@ -174,6 +174,15 @@ es:
investment_proyects: Ver lista completa de proyectos de gasto
unfeasible_investment_proyects: Ver lista de proyectos de gasto inviables
not_selected_investment_proyects: Ver lista de proyectos de gasto no seleccionados para la votación final
executions:
link: "Seguimiento"
page_title: "%{budget} - Seguimiento de proyectos"
heading: "Seguimiento de proyectos"
heading_selection_title: "Ámbito de actuación"
no_winner_investments: "No hay proyectos de gasto ganadores en este estado"
filters:
label: "Estado actual del proyecto"
all: "Todos (%{count})"
phases:
errors:
dates_range_invalid: "La fecha de comienzo no puede ser igual o superior a la de finalización"

View File

@@ -15,6 +15,7 @@ resources :budgets, only: [:show, :index] do
end
resource :results, only: :show, controller: "budgets/results"
resource :executions, only: :show, controller: 'budgets/executions'
end
scope '/participatory_budget' do

View File

@@ -139,6 +139,13 @@ section "Creating Valuation Assignments" do
end
end
section "Creating default Investment Milestone Statuses" do
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.studying_project'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.bidding'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.executing_project'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.executed'))
end
section "Creating investment milestones" do
Budget::Investment.find_each do |investment|
milestone = Budget::Investment::Milestone.new(investment_id: investment.id, publication_date: Date.tomorrow)
@@ -151,10 +158,3 @@ section "Creating investment milestones" do
end
end
end
section "Creating default Investment Milestone Statuses" do
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.studying_project'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.bidding'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.executing_project'))
Budget::Investment::Status.create(name: I18n.t('seeds.budgets.statuses.executed'))
end

View File

@@ -124,6 +124,15 @@ feature 'Budgets' do
expect(page).to have_content "There are no budgets"
end
scenario "Accepting" do
budget.update(phase: "accepting")
login_as(create(:user, :level_two))
visit budgets_path
expect(page).to have_link "Create a budget investment"
end
end
scenario 'Index shows only published phases' do

View File

@@ -0,0 +1,252 @@
require 'rails_helper'
feature 'Executions' do
let(:budget) { create(:budget, phase: 'finished') }
let(:group) { create(:budget_group, budget: budget) }
let(:heading) { create(:budget_heading, group: group) }
let!(:investment1) { create(:budget_investment, :winner, heading: heading) }
let!(:investment2) { create(:budget_investment, :winner, heading: heading) }
let!(:investment4) { create(:budget_investment, :winner, heading: heading) }
let!(:investment3) { create(:budget_investment, :incompatible, heading: heading) }
scenario 'only displays investments with milestones' do
create(:budget_investment_milestone, investment: investment1)
visit budget_path(budget)
click_link 'See results'
expect(page).to have_link('Milestones')
click_link 'Milestones'
expect(page).to have_content(investment1.title)
expect(page).not_to have_content(investment2.title)
expect(page).not_to have_content(investment3.title)
expect(page).not_to have_content(investment4.title)
end
scenario "Do not display headings with no winning investments for selected status" do
create(:budget_investment_milestone, investment: investment1)
empty_group = create(:budget_group, budget: budget)
empty_heading = create(:budget_heading, group: empty_group, price: 1000)
visit budget_path(budget)
click_link 'See results'
expect(page).to have_content(heading.name)
expect(page).to have_content(empty_heading.name)
click_link 'Milestones'
expect(page).to have_content(heading.name)
expect(page).not_to have_content(empty_heading.name)
end
scenario "Show message when there are no winning investments with the selected status", :js do
create(:budget_investment_status, name: I18n.t('seeds.budgets.statuses.executed'))
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).not_to have_content('No winner investments in this state')
select 'Executed (0)', from: 'status'
expect(page).to have_content('No winner investments in this state')
end
context 'Images' do
scenario 'renders milestone image if available' do
milestone1 = create(:budget_investment_milestone, investment: investment1)
create(:image, imageable: milestone1)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content(investment1.title)
expect(page).to have_css("img[alt='#{milestone1.image.title}']")
end
scenario 'renders investment image if no milestone image is available' do
create(:budget_investment_milestone, investment: investment2)
create(:image, imageable: investment2)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content(investment2.title)
expect(page).to have_css("img[alt='#{investment2.image.title}']")
end
scenario 'renders default image if no milestone nor investment images are available' do
create(:budget_investment_milestone, investment: investment4)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content(investment4.title)
expect(page).to have_css("img[alt='#{investment4.title}']")
end
scenario "renders last milestone's image if investment has multiple milestones with images associated" do
milestone1 = create(:budget_investment_milestone, investment: investment1,
publication_date: 2.weeks.ago)
milestone2 = create(:budget_investment_milestone, investment: investment1,
publication_date: Date.yesterday)
create(:image, imageable: milestone1, title: 'First milestone image')
create(:image, imageable: milestone2, title: 'Second milestone image')
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content(investment1.title)
expect(page).to have_css("img[alt='#{milestone2.image.title}']")
expect(page).not_to have_css("img[alt='#{milestone1.image.title}']")
end
end
context 'Filters' do
let!(:status1) { create(:budget_investment_status, name: I18n.t('seeds.budgets.statuses.studying_project')) }
let!(:status2) { create(:budget_investment_status, name: I18n.t('seeds.budgets.statuses.bidding')) }
scenario 'Filters select with counter are shown' do
create(:budget_investment_milestone, investment: investment1,
publication_date: Date.yesterday,
status: status1)
create(:budget_investment_milestone, investment: investment2,
publication_date: Date.yesterday,
status: status2)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content("All (2)")
expect(page).to have_content("#{status1.name} (1)")
expect(page).to have_content("#{status2.name} (1)")
end
scenario 'by milestone status', :js do
create(:budget_investment_milestone, investment: investment1, status: status1)
create(:budget_investment_milestone, investment: investment2, status: status2)
create(:budget_investment_status, name: I18n.t('seeds.budgets.statuses.executing_project'))
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
expect(page).to have_content(investment1.title)
expect(page).to have_content(investment2.title)
select 'Studying the project (1)', from: 'status'
expect(page).to have_content(investment1.title)
expect(page).not_to have_content(investment2.title)
select 'Bidding (1)', from: 'status'
expect(page).to have_content(investment2.title)
expect(page).not_to have_content(investment1.title)
select 'Executing the project (0)', from: 'status'
expect(page).not_to have_content(investment1.title)
expect(page).not_to have_content(investment2.title)
end
scenario 'are based on latest milestone status', :js do
create(:budget_investment_milestone, investment: investment1,
publication_date: 1.month.ago,
status: status1)
create(:budget_investment_milestone, investment: investment1,
publication_date: Date.yesterday,
status: status2)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
select 'Studying the project (0)', from: 'status'
expect(page).not_to have_content(investment1.title)
select 'Bidding (1)', from: 'status'
expect(page).to have_content(investment1.title)
end
scenario 'milestones with future dates are not shown', :js do
create(:budget_investment_milestone, investment: investment1,
publication_date: Date.yesterday,
status: status1)
create(:budget_investment_milestone, investment: investment1,
publication_date: Date.tomorrow,
status: status2)
visit budget_path(budget)
click_link 'See results'
click_link 'Milestones'
select 'Studying the project (1)', from: 'status'
expect(page).to have_content(investment1.title)
select 'Bidding (0)', from: 'status'
expect(page).not_to have_content(investment1.title)
end
end
context 'Heading Order' do
def create_heading_with_investment_with_milestone(group:, name:)
heading = create(:budget_heading, group: group, name: name)
investment = create(:budget_investment, :winner, heading: heading)
milestone = create(:budget_investment_milestone, investment: investment)
heading
end
scenario 'City heading is displayed first' do
heading.destroy!
other_heading1 = create_heading_with_investment_with_milestone(group: group, name: 'Other 1')
city_heading = create_heading_with_investment_with_milestone(group: group, name: 'Toda la ciudad')
other_heading2 = create_heading_with_investment_with_milestone(group: group, name: 'Other 2')
visit budget_executions_path(budget)
expect(page).to have_css('.budget-execution', count: 3)
expect(city_heading.name).to appear_before(other_heading1.name)
expect(city_heading.name).to appear_before(other_heading2.name)
end
scenario 'Non-city headings are displayed in alphabetical order' do
heading.destroy!
z_heading = create_heading_with_investment_with_milestone(group: group, name: 'Zzz')
a_heading = create_heading_with_investment_with_milestone(group: group, name: 'Aaa')
m_heading = create_heading_with_investment_with_milestone(group: group, name: 'Mmm')
visit budget_executions_path(budget)
expect(page).to have_css('.budget-execution', count: 3)
expect(a_heading.name).to appear_before(m_heading.name)
expect(m_heading.name).to appear_before(z_heading.name)
end
end
end

View File

@@ -69,4 +69,16 @@ describe Budget::Investment::Milestone do
end
end
describe ".published" do
it "uses the application's time zone date", :with_different_time_zone do
published_in_local_time_zone = create(:budget_investment_milestone,
publication_date: Date.today)
published_in_application_time_zone = create(:budget_investment_milestone,
publication_date: Date.current)
expect(Budget::Investment::Milestone.published).to include(published_in_application_time_zone)
expect(Budget::Investment::Milestone.published).not_to include(published_in_local_time_zone)
end
end
end

View File

@@ -90,6 +90,17 @@ RSpec.configure do |config|
travel_back
end
config.before(:each, :with_different_time_zone) do
system_zone = ActiveSupport::TimeZone.new("UTC")
local_zone = ActiveSupport::TimeZone.new("Madrid")
# Make sure the date defined by `config.time_zone` and
# the local date are different.
allow(Time).to receive(:zone).and_return(system_zone)
allow(Time).to receive(:now).and_return(Date.current.at_end_of_day.in_time_zone(local_zone))
allow(Date).to receive(:today).and_return(Time.now.to_date)
end
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options.
config.example_status_persistence_file_path = "spec/examples.txt"