Merge pull request #3195 from consul/milestone_progress_bar

[Backport] Manage milestone progress bars
This commit is contained in:
Alberto
2019-01-24 11:59:24 +01:00
committed by GitHub
30 changed files with 659 additions and 15 deletions

View File

@@ -23,8 +23,29 @@ App.Forms =
false false
) )
synchronizeInputs: ->
$("[name='progress_bar[percentage]']").on
input: ->
$("[name='#{this.name}']").val($(this).val())
$("[name='progress_bar[percentage]'][type='range']").trigger("input")
hideOrShowFieldsAfterSelection: ->
$("[name='progress_bar[kind]']").on
change: ->
title_field = $("[name^='progress_bar'][name$='[title]']").parent()
if this.value == "primary"
title_field.hide()
else
title_field.show()
$("[name='progress_bar[kind]']").change()
initialize: -> initialize: ->
App.Forms.disableEnter() App.Forms.disableEnter()
App.Forms.submitOnChange() App.Forms.submitOnChange()
App.Forms.toggleLink() App.Forms.toggleLink()
App.Forms.synchronizeInputs()
App.Forms.hideOrShowFieldsAfterSelection()
false false

View File

@@ -251,6 +251,13 @@ $sidebar-active: #f4fcd0;
max-width: none; max-width: none;
} }
form {
.input-group-label {
height: $line-height * 2;
}
}
.menu.simple { .menu.simple {
margin-bottom: $line-height / 2; margin-bottom: $line-height / 2;

View File

@@ -0,0 +1,8 @@
class Admin::BudgetInvestmentProgressBarsController < Admin::ProgressBarsController
private
def progressable
Budget::Investment.find(params[:budget_investment_id])
end
end

View File

@@ -0,0 +1,14 @@
class Admin::Legislation::ProgressBarsController < Admin::ProgressBarsController
include FeatureFlags
feature_flag :legislation
def index
@process = progressable
end
private
def progressable
::Legislation::Process.find(params[:process_id])
end
end

View File

@@ -0,0 +1,69 @@
class Admin::ProgressBarsController < Admin::BaseController
include Translatable
before_action :load_progressable
before_action :load_progress_bar, only: [:edit, :update, :destroy]
helper_method :progress_bars_index
def index
end
def new
@progress_bar = @progressable.progress_bars.new
end
def create
@progress_bar = @progressable.progress_bars.new(progress_bar_params)
if @progress_bar.save
redirect_to progress_bars_index, notice: t("admin.progress_bars.create.notice")
else
render :new
end
end
def edit
end
def update
if @progress_bar.update(progress_bar_params)
redirect_to progress_bars_index, notice: t('admin.progress_bars.update.notice')
else
render :edit
end
end
def destroy
@progress_bar.destroy
redirect_to progress_bars_index, notice: t('admin.progress_bars.delete.notice')
end
private
def progress_bar_params
params.require(:progress_bar).permit(allowed_params)
end
def allowed_params
[
:kind,
:percentage,
translation_params(ProgressBar)
]
end
def load_progressable
@progressable = progressable
end
def progressable
raise "This method must be implemented in subclass #{self.class.name}"
end
def load_progress_bar
@progress_bar = progressable.progress_bars.find(params[:id])
end
def progress_bars_index
polymorphic_path([:admin, *resource_hierarchy_for(@progressable), ProgressBar.new])
end
end

View File

@@ -0,0 +1,7 @@
class Admin::ProposalProgressBarsController < Admin::ProgressBarsController
private
def progressable
Proposal.find(params[:proposal_id])
end
end

View File

@@ -5,5 +5,7 @@ module Milestoneable
has_many :milestones, as: :milestoneable, dependent: :destroy has_many :milestones, as: :milestoneable, dependent: :destroy
scope :with_milestones, -> { joins(:milestones).distinct } scope :with_milestones, -> { joins(:milestones).distinct }
has_many :progress_bars, as: :progressable
end end
end end

View File

@@ -0,0 +1,28 @@
class ProgressBar < ActiveRecord::Base
self.inheritance_column = nil
RANGE = 0..100
enum kind: %i[primary secondary]
belongs_to :progressable, polymorphic: true
translates :title, touch: true
include Globalizable
validates :progressable, presence: true
validates :kind, presence: true,
uniqueness: {
scope: [:progressable_type, :progressable_id],
conditions: -> { primary }
}
validates :percentage, presence: true, inclusion: RANGE, numericality: { only_integer: true }
before_validation :assign_progress_bar_to_translations
validates_translation :title, presence: true, unless: :primary?
private
def assign_progress_bar_to_translations
translations.each { |translation| translation.globalized_model = self }
end
end

View File

@@ -0,0 +1,3 @@
class ProgressBar::Translation < Globalize::ActiveRecord::Translation
delegate :primary?, to: :globalized_model
end

View File

@@ -0,0 +1,12 @@
<% provide :title do %>
<%= "#{t("admin.header.title")} - #{t("admin.menu.legislation")}" %> -
<%= "#{@process.title} - #{t("admin.progress_bars.index.title")}" %>
<% end %>
<%= back_link_to admin_legislation_process_milestones_path(@progressable),
t("admin.legislation.processes.edit.back") %>
<h2><%= @process.title %></h2>
<%= render "admin/legislation/processes/subnav", process: @process, active: "milestones" %>
<%= render "admin/progress_bars/progress_bars", progressable: @process %>

View File

@@ -1,4 +1,8 @@
<h2><%= t("admin.milestones.index.milestone") %></h2> <h2 class="inline-block"><%= t("admin.milestones.index.milestone") %></h2>
<%= link_to t("admin.progress_bars.manage"),
polymorphic_path([:admin, *resource_hierarchy_for(milestoneable.progress_bars.new)]),
class: "button hollow float-right" %>
<% if milestoneable.milestones.any? %> <% if milestoneable.milestones.any? %>
<table> <table>

View File

@@ -0,0 +1,31 @@
<%= render "admin/shared/globalize_locales", resource: @progress_bar %>
<%= translatable_form_for [:admin, *resource_hierarchy_for(@progress_bar)] do |f| %>
<div class="small-12 medium-6">
<%= f.enum_select :kind %>
</div>
<%= f.translatable_fields do |translations_form| %>
<div class="small-12 medium-6">
<%= translations_form.text_field :title %>
</div>
<% end %>
<% progress_options = { min: ProgressBar::RANGE.min, max: ProgressBar::RANGE.max, step: 1 } %>
<div class="small-12 medium-6 large-2">
<%= f.text_field :percentage, { type: :range,
id: "percentage_range",
class: "column" }.merge(progress_options) %>
</div>
<div class="small-12 medium-6 large-2">
<div class="input-group">
<%= f.text_field :percentage, { type: :number,
label: false,
class: "input-group-field" }.merge(progress_options) %>
<span class="input-group-label">%</span>
</div>
</div>
<%= f.submit nil, class: "button success" %>
<% end %>

View File

@@ -0,0 +1,57 @@
<h2 class="inline-block"><%= t("admin.progress_bars.index.title") %></h2>
<%= link_to t("admin.progress_bars.index.new_progress_bar"),
polymorphic_path(
[:admin, *resource_hierarchy_for(ProgressBar.new(progressable: progressable))],
action: :new
),
class: "button float-right" %>
<% if progressable.progress_bars.any? %>
<table>
<thead>
<tr>
<th><%= t("admin.progress_bars.index.table_id") %></th>
<th><%= t("admin.progress_bars.index.table_kind") %></th>
<th><%= t("admin.progress_bars.index.table_title") %></th>
<th class="text-center"><%= t("admin.progress_bars.index.table_percentage") %></th>
<th><%= t("admin.actions.actions") %></th>
</tr>
</thead>
<tbody>
<% progressable.progress_bars.each do |progress_bar| %>
<tr id="<%= dom_id(progress_bar) %>" class="progress-bar">
<td>
<%= progress_bar.id %>
</td>
<td><%= ProgressBar.human_attribute_name("kind.#{progress_bar.kind}") %></td>
<td>
<% if progress_bar.title.present? %>
<%= progress_bar.title %>
<% else %>
<strong><%= t("admin.progress_bars.index.primary") %></strong>
<% end %>
</td>
<td class="text-center">
<%= number_to_percentage(progress_bar.percentage, strip_insignificant_zeros: true) %>
</td>
<td>
<%= link_to t("admin.actions.edit"),
polymorphic_path([:admin, *resource_hierarchy_for(progress_bar)],
action: :edit),
class: "button hollow" %>
<%= link_to t("admin.actions.delete"),
polymorphic_path([:admin, *resource_hierarchy_for(progress_bar)]),
method: :delete,
class: "button hollow alert" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<div class="callout primary">
<%= t("admin.progress_bars.index.no_progress_bars") %>
</div>
<% end %>

View File

@@ -0,0 +1,15 @@
<% if @progress_bar.primary? %>
<% bar_title = t("admin.progress_bars.edit.title.primary") %>
<% else %>
<% bar_title = t("admin.progress_bars.edit.title.secondary", title: @progress_bar.title) %>
<% end %>
<% provide :title do %>
<%= "#{t("admin.header.title")} - #{bar_title}" %>
<% end %>
<%= back_link_to progress_bars_index %>
<h2><%= bar_title %></h2>
<%= render "form" %>

View File

@@ -0,0 +1,9 @@
<% provide :title do %>
<%= "#{t("admin.header.title")} - #{t("admin.progress_bars.index.title")}" %>
<% end %>
<%= back_link_to polymorphic_path([:admin, *resource_hierarchy_for(@progressable)]) %>
<div class="clear"></div>
<%= render "admin/progress_bars/progress_bars", progressable: @progressable %>

View File

@@ -0,0 +1,9 @@
<% provide :title do %>
<%= "#{t("admin.header.title")} - #{t("admin.progress_bars.new.creating")}" %>
<% end %>
<%= back_link_to progress_bars_index %>
<h2><%= t("admin.progress_bars.new.creating") %></h2>
<%= render "form" %>

View File

@@ -5,5 +5,13 @@ module FoundationRailsHelper
super(attribute, opts) super(attribute, opts)
end end
end end
def enum_select(attribute, options = {}, html_options = {})
choices = object.class.send(attribute.to_s.pluralize).keys.map do |name|
[object.class.human_attribute_name("#{attribute}.#{name}"), name]
end
select attribute, choices, options, html_options
end
end end
end end

View File

@@ -9,6 +9,8 @@ module ActionDispatch::Routing::UrlFor
[resource.budget, resource] [resource.budget, resource]
when "Milestone" when "Milestone"
[*resource_hierarchy_for(resource.milestoneable), resource] [*resource_hierarchy_for(resource.milestoneable), resource]
when "ProgressBar"
[*resource_hierarchy_for(resource.progressable), resource]
when "Legislation::Annotation" when "Legislation::Annotation"
[resource.draft_version.process, resource.draft_version, resource] [resource.draft_version.process, resource.draft_version, resource]
when "Legislation::Proposal", "Legislation::Question", "Legislation::DraftVersion" when "Legislation::Proposal", "Legislation::Question", "Legislation::DraftVersion"

View File

@@ -16,6 +16,9 @@ en:
milestone/status: milestone/status:
one: "Milestone Status" one: "Milestone Status"
other: "Milestone Statuses" other: "Milestone Statuses"
progress_bar:
one: "Progress bar"
other: "Progress bars"
comment: comment:
one: "Comment" one: "Comment"
other: "Comments" other: "Comments"
@@ -136,6 +139,13 @@ en:
milestone/status: milestone/status:
name: "Name" name: "Name"
description: "Description (optional)" description: "Description (optional)"
progress_bar:
kind: "Type"
title: "Title"
percentage: "Current progress"
progress_bar/kind:
primary: "Primary"
secondary: "Secondary"
budget/heading: budget/heading:
name: "Heading name" name: "Heading name"
price: "Price" price: "Price"

View File

@@ -332,6 +332,29 @@ en:
notice: Milestone status created successfully notice: Milestone status created successfully
delete: delete:
notice: Milestone status deleted successfully notice: Milestone status deleted successfully
progress_bars:
manage: "Manage progress bars"
index:
title: "Progress bars"
no_progress_bars: "There are no progress bars"
new_progress_bar: "Create new progress bar"
primary: "Primary progress bar"
table_id: "ID"
table_kind: "Type"
table_title: "Title"
table_percentage: "Current progress"
new:
creating: "Create progress bar"
edit:
title:
primary: "Edit primary progress bar"
secondary: "Edit progress bar %{title}"
create:
notice: "Progress bar created successfully!"
update:
notice: "Progress bar updated successfully"
delete:
notice: "Progress bar deleted successfully"
comments: comments:
index: index:
filter: Filter filter: Filter

View File

@@ -16,6 +16,9 @@ es:
milestone/status: milestone/status:
one: "Estado de seguimiento" one: "Estado de seguimiento"
other: "Estados de seguimiento" other: "Estados de seguimiento"
progress_bar:
one: "Barra de progreso"
other: "Barras de progreso"
comment: comment:
one: "Comentario" one: "Comentario"
other: "Comentarios" other: "Comentarios"
@@ -136,6 +139,13 @@ es:
milestone/status: milestone/status:
name: "Nombre" name: "Nombre"
description: "Descripción (opcional)" description: "Descripción (opcional)"
progress_bar:
kind: "Tipo"
title: "Título"
percentage: "Progreso"
progress_bar/kind:
primary: "Principal"
secondary: "Secundaria"
budget/heading: budget/heading:
name: "Nombre de la partida" name: "Nombre de la partida"
price: "Cantidad" price: "Cantidad"

View File

@@ -332,6 +332,30 @@ es:
notice: Estado de seguimiento creado correctamente notice: Estado de seguimiento creado correctamente
delete: delete:
notice: Estado de seguimiento eliminado correctamente notice: Estado de seguimiento eliminado correctamente
progress_bars:
manage: "Gestionar barras de progreso"
index:
title: "Barras de progreso"
no_progress_bars: "No hay barras de progreso"
new_progress_bar: "Crear nueva barra de progreso"
primary: "Barra de progreso principal"
table_id: "ID"
table_kind: "Tipo"
table_title: "Título"
table_percentage: "Progreso"
new:
creating: "Crear barra de progreso"
edit:
title:
primary: "Editar barra de progreso principal"
secondary: "Editar barra de progreso %{title}"
create:
notice: "¡Barra de progreso creada con éxito!"
update:
notice: "Barra de progreso actualizada"
delete:
notice: "Barra de progreso eliminada correctamente"
comments: comments:
index: index:
filter: Filtro filter: Filtro

View File

@@ -31,6 +31,7 @@ namespace :admin do
resources :proposals, only: [:index, :show] do resources :proposals, only: [:index, :show] do
resources :milestones, controller: "proposal_milestones" resources :milestones, controller: "proposal_milestones"
resources :progress_bars, except: :show, controller: "proposal_progress_bars"
end end
resources :hidden_proposals, only: :index do resources :hidden_proposals, only: :index do
@@ -67,6 +68,7 @@ namespace :admin do
resources :budget_investments, only: [:index, :show, :edit, :update] do resources :budget_investments, only: [:index, :show, :edit, :update] do
resources :milestones, controller: 'budget_investment_milestones' resources :milestones, controller: 'budget_investment_milestones'
resources :progress_bars, except: :show, controller: "budget_investment_progress_bars"
member { patch :toggle_selection } member { patch :toggle_selection }
end end
@@ -203,6 +205,7 @@ namespace :admin do
end end
resources :draft_versions resources :draft_versions
resources :milestones resources :milestones
resources :progress_bars, except: :show
resource :homepage, only: [:edit, :update] resource :homepage, only: [:edit, :update]
end end
end end

View File

@@ -0,0 +1,23 @@
class CreateProgressBars < ActiveRecord::Migration
def change
create_table :progress_bars do |t|
t.integer :kind
t.integer :percentage
t.references :progressable, polymorphic: true
t.timestamps null: false
end
reversible do |change|
change.up do
ProgressBar.create_translation_table!({
title: :string
})
end
change.down do
ProgressBar.drop_translation_table!
end
end
end
end

View File

@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20181206153510) do ActiveRecord::Schema.define(version: 20190103132925) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -1088,6 +1088,26 @@ ActiveRecord::Schema.define(version: 20181206153510) do
add_index "polls", ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at", using: :btree add_index "polls", ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at", using: :btree
create_table "progress_bar_translations", force: :cascade do |t|
t.integer "progress_bar_id", null: false
t.string "locale", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "title"
end
add_index "progress_bar_translations", ["locale"], name: "index_progress_bar_translations_on_locale", using: :btree
add_index "progress_bar_translations", ["progress_bar_id"], name: "index_progress_bar_translations_on_progress_bar_id", using: :btree
create_table "progress_bars", force: :cascade do |t|
t.integer "kind"
t.integer "percentage"
t.integer "progressable_id"
t.string "progressable_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "proposal_notifications", force: :cascade do |t| create_table "proposal_notifications", force: :cascade do |t|
t.string "title" t.string "title"
t.text "body" t.text "body"

View File

@@ -195,19 +195,6 @@ FactoryBot.define do
reason "unfeasible" reason "unfeasible"
end end
factory :milestone_status, class: 'Milestone::Status' do
sequence(:name) { |n| "Milestone status #{n} name" }
sequence(:description) { |n| "Milestone status #{n} description" }
end
factory :milestone, class: 'Milestone' do
association :milestoneable, factory: :budget_investment
association :status, factory: :milestone_status
sequence(:title) { |n| "Budget investment milestone #{n} title" }
description 'Milestone description'
publication_date { Date.current }
end
factory :valuator_group, class: ValuatorGroup do factory :valuator_group, class: ValuatorGroup do
sequence(:name) { |n| "Valuator Group #{n}" } sequence(:name) { |n| "Valuator Group #{n}" }
end end

View File

@@ -0,0 +1,25 @@
FactoryBot.define do
factory :milestone_status, class: "Milestone::Status" do
sequence(:name) { |n| "Milestone status #{n} name" }
sequence(:description) { |n| "Milestone status #{n} description" }
end
factory :milestone do
association :milestoneable, factory: :budget_investment
association :status, factory: :milestone_status
sequence(:title) { |n| "Milestone #{n} title" }
description "Milestone description"
publication_date { Date.current }
end
factory :progress_bar do
association :progressable, factory: :budget_investment
percentage { rand(0..100) }
kind :primary
trait(:secondary) do
kind :secondary
sequence(:title) { |n| "Progress bar #{n} title" }
end
end
end

View File

@@ -0,0 +1,97 @@
require "rails_helper"
describe ProgressBar do
let(:progress_bar) { build(:progress_bar) }
it "is valid" do
expect(progress_bar).to be_valid
end
it "is valid without a title" do
progress_bar.title = nil
expect(progress_bar).to be_valid
end
it "is not valid with a custom type" do
expect { progress_bar.kind = "terciary" }.to raise_exception(ArgumentError)
end
it "is not valid without a percentage" do
progress_bar.percentage = nil
expect(progress_bar).not_to be_valid
end
it "is not valid with a non-numeric percentage" do
progress_bar.percentage = "High"
expect(progress_bar).not_to be_valid
end
it "is not valid with a non-integer percentage" do
progress_bar.percentage = 22.83
expect(progress_bar).not_to be_valid
end
it "is not valid with a negative percentage" do
progress_bar.percentage = -1
expect(progress_bar).not_to be_valid
end
it "is not valid with a percentage bigger than 100" do
progress_bar.percentage = 101
expect(progress_bar).not_to be_valid
end
it "is valid with an integer percentage within the limits" do
progress_bar.percentage = 0
expect(progress_bar).to be_valid
progress_bar.percentage = 100
expect(progress_bar).to be_valid
progress_bar.percentage = 83
expect(progress_bar).to be_valid
end
it "is not valid without a progressable" do
progress_bar.progressable = nil
expect(progress_bar).not_to be_valid
end
it "cannot have another primary progress bar for the same progressable" do
progress_bar.save
duplicate = build(:progress_bar, progressable: progress_bar.progressable)
expect(duplicate).not_to be_valid
end
describe "secondary progress bar" do
let(:progress_bar) { build(:progress_bar, :secondary) }
it "is valid" do
expect(progress_bar).to be_valid
end
it "is invalid without a title" do
progress_bar.title = nil
expect(progress_bar).not_to be_valid
end
it "can have another secondary progress bar for the same progressable" do
progress_bar.save
duplicate = build(:progress_bar, progressable: progress_bar.progressable)
expect(duplicate).to be_valid
end
end
end

View File

@@ -1,4 +1,5 @@
shared_examples "admin_milestoneable" do |factory_name, path_name| shared_examples "admin_milestoneable" do |factory_name, path_name|
it_behaves_like "progressable", factory_name, path_name
feature "Admin milestones" do feature "Admin milestones" do
let!(:milestoneable) { create(factory_name) } let!(:milestoneable) { create(factory_name) }

View File

@@ -0,0 +1,115 @@
shared_examples "progressable" do |factory_name, path_name|
let!(:progressable) { create(factory_name) }
feature "Manage progress bars" do
let(:progressable_path) { send(path_name, *resource_hierarchy_for(progressable)) }
let(:path) do
polymorphic_path([:admin, *resource_hierarchy_for(progressable.progress_bars.new)])
end
context "Index" do
scenario "Link to index path" do
create(:progress_bar, :secondary, progressable: progressable,
title: "Reading documents",
percentage: 20)
visit progressable_path
click_link "Manage progress bars"
expect(page).to have_content "Reading documents"
end
scenario "No progress bars" do
visit path
expect(page).to have_content("There are no progress bars")
end
end
context "New" do
scenario "Primary progress bar", :js do
visit path
click_link "Create new progress bar"
select "Primary", from: "Type"
expect(page).not_to have_field "Title"
fill_in "Current progress", with: 43
click_button "Create Progress bar"
expect(page).to have_content "Progress bar created successfully"
expect(page).to have_content "43%"
expect(page).to have_content "Primary"
expect(page).to have_content "Primary progress bar"
end
scenario "Secondary progress bar", :js do
visit path
click_link "Create new progress bar"
select "Secondary", from: "Type"
fill_in "Current progress", with: 36
fill_in "Title", with: "Plant trees"
click_button "Create Progress bar"
expect(page).to have_content "Progress bar created successfully"
expect(page).to have_content "36%"
expect(page).to have_content "Secondary"
expect(page).to have_content "Plant trees"
end
end
context "Edit" do
scenario "Primary progress bar", :js do
bar = create(:progress_bar, progressable: progressable)
visit path
within("#progress_bar_#{bar.id}") { click_link "Edit" }
expect(page).to have_field "Current progress"
expect(page).not_to have_field "Title"
fill_in "Current progress", with: 44
click_button "Update Progress bar"
expect(page).to have_content "Progress bar updated successfully"
within("#progress_bar_#{bar.id}") do
expect(page).to have_content "44%"
end
end
scenario "Secondary progress bar", :js do
bar = create(:progress_bar, :secondary, progressable: progressable)
visit path
within("#progress_bar_#{bar.id}") { click_link "Edit" }
fill_in "Current progress", with: 76
fill_in "Title", with: "Updated title"
click_button "Update Progress bar"
expect(page).to have_content "Progress bar updated successfully"
within("#progress_bar_#{bar.id}") do
expect(page).to have_content "76%"
expect(page).to have_content "Updated title"
end
end
end
context "Delete" do
scenario "Remove progress bar" do
bar = create(:progress_bar, progressable: progressable, percentage: 34)
visit path
within("#progress_bar_#{bar.id}") { click_link "Delete" }
expect(page).to have_content "Progress bar deleted successfully"
expect(page).not_to have_content "34%"
end
end
end
end