Adds an administration interface for the proposal dashboard actions.
This commit is contained in:
Juan Salvador Pérez García
2018-06-11 18:26:18 +02:00
parent d9f85bd756
commit 1464bddfa8
26 changed files with 619 additions and 1 deletions

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the Admin::ProposalDashboardActions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
class Admin::ProposalDashboardActionsController < Admin::BaseController
helper_method :proposal_dashboard_action
def index
@proposal_dashboard_actions = ProposalDashboardAction.all
end
def new
@proposal_dashboard_action = ProposalDashboardAction.new(active: true, day_offset: 0, required_supports: 0, request_to_administrators: true)
end
def create
@proposal_dashboard_action = ProposalDashboardAction.new(proposal_dashboard_action_params)
if @proposal_dashboard_action.save
redirect_to admin_proposal_dashboard_actions_path, notice: t('admin.proposal_dashboard_actions.create.notice')
else
render :new
end
end
def edit; end
def update
if proposal_dashboard_action.update(proposal_dashboard_action_params)
redirect_to admin_proposal_dashboard_actions_path
else
render :edit
end
end
def destroy
proposal_dashboard_action.destroy
redirect_to admin_proposal_dashboard_actions_path, notice: t('admin.proposal_dashboard_actions.delete.success')
end
private
def proposal_dashboard_action_params
params
.require(:proposal_dashboard_action)
.permit(:title, :description, :link, :request_to_administrators, :day_offset, :required_supports, :order, :active)
end
def proposal_dashboard_action
@proposal_dashboard_action ||= ProposalDashboardAction.find(params[:id])
end
end

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Admin::ProposalDashboardActionsHelper
def active_human_readable(active)
return t('admin.proposal_dashboard_actions.index.active') if active
t('admin.proposal_dashboard_actions.index.inactive')
end
end

View File

@@ -48,6 +48,7 @@ module Abilities
can [:search, :index], ::User can [:search, :index], ::User
can :manage, Annotation can :manage, Annotation
can :manage, ProposalDashboardAction
can [:read, :update, :valuate, :destroy, :summary], SpendingProposal can [:read, :update, :valuate, :destroy, :summary], SpendingProposal

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
class ProposalDashboardAction < ActiveRecord::Base
acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases
validates :title,
presence: true,
allow_blank: false,
length: { in: 4..80 }
validates :description,
presence: true,
allow_blank: false,
length: { in: 4..255 }
validates :day_offset,
presence: true,
numericality: {
only_integer: true,
greater_than_or_equal_to: 0
}
validates :required_supports,
presence: true,
numericality: {
only_integer: true,
greater_than_or_equal_to: 0
}
validates :link,
presence: true,
allow_blank: false,
unless: :request_to_administrators?
default_scope { order(order: :asc, title: :asc) }
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
def request_to_administrators?
request_to_administrators || false
end
end

View File

@@ -223,6 +223,12 @@
<%= link_to t("admin.menu.geozones"), admin_geozones_path %> <%= link_to t("admin.menu.geozones"), admin_geozones_path %>
</li> </li>
<li <%= 'class=is_active' if controller_name == 'proposal_dashboard_actions' %>>
<%= link_to <%= ProposalDashboardAction.model_name.human(count: 2),
admin_proposal_dashboard_actions_path %>
</li>
<li <%= "class=is-active" if controller_name == "images" && <li <%= "class=is-active" if controller_name == "images" &&
controller.class.parent != Admin::Poll::Questions::Answers %>> controller.class.parent != Admin::Poll::Questions::Answers %>>
<%= link_to t("admin.menu.site_customization.images"), admin_site_customization_images_path %> <%= link_to t("admin.menu.site_customization.images"), admin_site_customization_images_path %>

View File

@@ -0,0 +1,13 @@
<% if proposal_dashboard_action.errors.any? %>
<div id="error_explanation" data-alert class="callout alert" data-closable>
<button class="close-button" aria-label="<%= t('application.close') %>" type="button" data-close>
<span aria-hidden="true">&times;</span>
</button>
<strong>
<%= proposal_dashboard_action.errors.count %>
<%= t('admin.proposal_dashboard_actions.errors.form.error', count: proposal_dashboard_action.errors.count) %>
</strong>
</div>
<% end %>

View File

@@ -0,0 +1,56 @@
<%= form_for [:admin, proposal_dashboard_action] do |f| %>
<%= render 'errors' %>
<div class="row">
<div class="small-12 column">
<%= f.label :title %>
<%= f.text_field :title, label: false %>
</div>
</div>
<div class="row">
<div class="small-12 column">
<%= f.label :description %>
<%= f.text_field :description, label: false %>
</div>
</div>
<div class="small-12 column">
<%= f.check_box :request_to_administrators, label: ProposalDashboardAction.human_attribute_name(:request_to_administrators) %>
</div>
<div class="row">
<div class="small-12 column">
<%= f.label :link%>
<%= f.text_field :link, label: false %>
</div>
</div>
<div class="row">
<div class="small-12 medium-4 large-4 column">
<%= f.label :day_offset %>
<%= f.number_field :day_offset, label: false, step: 1, min: 0 %>
</div>
<div class="small-12 medium-4 large-4 column">
<%= f.label :required_supports %>
<%= f.number_field :required_supports, label: false, step: 1, min: 0 %>
</div>
<div class="small-12 medium-4 large-4 column">
<%= f.label :order %>
<%= f.number_field :order, label: false, step: 1, min: 0 %>
</div>
</div>
<div class="small-12 column">
<%= f.check_box :active, label: ProposalDashboardAction.human_attribute_name(:active) %>
</div>
<div class="row">
<div class="actions small-12 large-3 medium-3 column">
<%= f.submit(class: 'button expanded', value: t('admin.proposal_dashboard_actions.form.submit_button')) %>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,9 @@
<div class="row">
<div class="small-12 column">
<%= back_link_to admin_proposal_dashboard_actions_path, t('admin.proposal_dashboard_actions.edit.back') %>
<h1><%= t('admin.proposal_dashboard_actions.edit.editing') %></h1>
<%= render 'form' %>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<%= link_to t('admin.proposal_dashboard_actions.index.create'),
new_admin_proposal_dashboard_action_path, class: 'button success float-right' %>
<h2 class="inline-block">
<%= ProposalDashboardAction.model_name.human(count: 2) %>
</h2>
<table>
<thead>
<tr>
<th><%= ProposalDashboardAction.human_attribute_name(:title) %></th>
<th><%= ProposalDashboardAction.human_attribute_name(:active) %></th>
<th></th>
</tr>
</thead>
<tbody>
<% if @proposal_dashboard_actions.empty? %>
<tr>
<td colspan="100%"><%= t 'admin.proposal_dashboard_actions.index.no_records' %></td>
</tr>
<% end %>
<% @proposal_dashboard_actions.each do |action| %>
<tr id="<%= dom_id(action) %>">
<td><%= action.title %></td>
<td><%= active_human_readable(action.active) %></td>
<td style="text-align: right">
<%= link_to t('admin.proposal_dashboard_actions.index.edit'),
edit_admin_proposal_dashboard_action_path(action),
class: 'edit-banner button hollow' %>
<%= link_to t('admin.proposal_dashboard_actions.index.delete'),
admin_proposal_dashboard_action_path(action),
method: :delete,
class: 'button hollow alert',
data: { confirm: t('admin.actions.confirm') } %>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,9 @@
<div class="proposal-dashboard-action-new row">
<div class="small-12 column">
<%= back_link_to admin_proposal_dashboard_actions_path, t('admin.proposal_dashboard_actions.new.back') %>
<h1><%= t('admin.proposal_dashboard_actions.new.creating') %></h1>
<%= render 'form' %>
</div>
</div>

View File

@@ -106,6 +106,9 @@ en:
proposal_notification: proposal_notification:
one: "Proposal notification" one: "Proposal notification"
other: "Proposal notifications" other: "Proposal notifications"
proposal_dashboard_action:
one: Proposal dashboard action
other: Proposal dashboard actions
attributes: attributes:
budget: budget:
name: "Name" name: "Name"
@@ -244,7 +247,6 @@ en:
poll/question/answer: poll/question/answer:
title: Answer title: Answer
description: Description description: Description
poll/question/answer/video:
title: Title title: Title
url: External video url: External video
newsletter: newsletter:
@@ -260,6 +262,15 @@ en:
link_url: Link URL link_url: Link URL
widget/feed: widget/feed:
limit: Number of items limit: Number of items
proposal_dashboard_action:
title: Title
description: Description
link: External link
request_to_administrators: Admin request
day_offset: Required days for the activation
required_supports: Required supports for the activation
order: Order
active: Active
errors: errors:
models: models:
user: user:

View File

@@ -1090,6 +1090,31 @@ en:
delete: delete:
success: Geozone successfully deleted success: Geozone successfully deleted
error: This geozone can't be deleted since there are elements attached to it error: This geozone can't be deleted since there are elements attached to it
proposal_dashboard_actions:
index:
create: Create
edit: Edit
delete: Delete
no_records: No records found
active: 'Yes'
inactive: 'No'
new:
creating: New action for the proposals dashboard
back: Back to list
create:
notice: Action created successfully
edit:
editing: Edit action for the proposals dashboard
back: Back to list
delete:
success: Action successfully deleted
form:
submit_button: Save
errors:
form:
error:
one: 'error prevented this action from being saved'
other: 'errors prevented this action from being saved'
signature_sheets: signature_sheets:
author: Author author: Author
created_at: Creation date created_at: Creation date

View File

@@ -106,6 +106,9 @@ es:
proposal_notification: proposal_notification:
one: "Notificación de propuesta" one: "Notificación de propuesta"
other: "Notificaciones de propuestas" other: "Notificaciones de propuestas"
proposal_dashboard_action:
one: Acción del panel de control de propuestas
other: Acciones del panel de control de propuestas
attributes: attributes:
budget: budget:
name: "Nombre" name: "Nombre"
@@ -260,6 +263,15 @@ es:
link_url: URL del enlace link_url: URL del enlace
widget/feed: widget/feed:
limit: Número de elementos limit: Número de elementos
proposal_dashboard_action:
title: Título
description: Descripción
link: Enlace externo
request_to_administrators: Petición para administrador
day_offset: Días requeridos para la activación
required_supports: Soportes requeridos para la activación
order: Orden
active: Activa
errors: errors:
models: models:
user: user:

View File

@@ -1090,6 +1090,32 @@ es:
delete: delete:
success: Distrito borrado correctamente success: Distrito borrado correctamente
error: No se puede borrar el distrito porque ya tiene elementos asociados error: No se puede borrar el distrito porque ya tiene elementos asociados
proposal_dashboard_actions:
index:
create: Crear
edit: Editar
delete: Borrar
no_records: No se encontraron registros
active: Si
inactive: No
new:
creating: Nueva acción para el dashboard de propuestas
back: Volver a la lista
create:
notice: Acción creada con éxito
edit:
editing: Editar acción para el dashboard de propuestas
back: Volver a la lista
form:
submit_button: Guardar
delete:
success: Acción borrada con éxito
errors:
form:
error:
one: "error impidió guardar la acción"
other: 'errores impidieron guardar la acción.'
signature_sheets: signature_sheets:
author: Autor author: Autor
created_at: Fecha de creación created_at: Fecha de creación

View File

@@ -195,4 +195,6 @@ namespace :admin do
resources :cards resources :cards
resources :feeds, only: [:update] resources :feeds, only: [:update]
end end
resources :proposal_dashboard_actions, only: %i[index new create edit update destroy]
end end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
class CreateProposalDashboardActions < ActiveRecord::Migration
def change
create_table :proposal_dashboard_actions do |t|
t.string :title, limit: 80
t.string :description
t.string :link
t.boolean :request_to_administrators, default: false
t.integer :day_offset, default: 0
t.integer :required_supports, default: 0
t.integer :order, default: 0
t.boolean :active, default: true
t.datetime :hidden_at
end
end
end

View File

@@ -878,6 +878,18 @@ ActiveRecord::Schema.define(version: 20180711224810) 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 "proposal_dashboard_actions", force: :cascade do |t|
t.string "title", limit: 80
t.string "description"
t.string "link"
t.boolean "request_to_administrators", default: false
t.integer "day_offset", default: 0
t.integer "required_supports", default: 0
t.integer "order", default: 0
t.boolean "active", default: true
t.datetime "hidden_at"
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

@@ -1029,4 +1029,37 @@ LOREM_IPSUM
factory :widget_feed, class: 'Widget::Feed' do factory :widget_feed, class: 'Widget::Feed' do
end end
factory :proposal_dashboard_action, class: 'ProposalDashboardAction' do
title { Faker::Lorem.sentence }
description { Faker::Lorem.sentence }
link nil
request_to_administrators true
day_offset 0
required_supports 0
order 0
active true
hidden_at nil
trait :admin_request do
link nil
request_to_administrators true
end
trait :external_link do
link { Faker::Internet.url }
request_to_administrators false
end
trait :inactive do
active false
end
trait :active do
active true
end
trait :deleted do
hidden_at { Time.now.utc }
end
end
end end

View File

@@ -0,0 +1,84 @@
require 'rails_helper'
feature 'Admin proposal dasboard actions' do
let(:admin) { create :administrator }
before do
login_as(admin.user)
end
context 'when visiting index' do
context 'and no actions defined' do
before do
visit admin_proposal_dashboard_actions_path
end
it 'shows that there are no records available' do
expect(page).to have_content('No records found')
end
end
context 'and actions defined' do
let!(:action) { create :proposal_dashboard_action }
before do
visit admin_proposal_dashboard_actions_path
end
it 'shows the action data' do
expect(page).to have_content(action.title)
end
end
end
context 'when creating an action' do
let(:action) { build :proposal_dashboard_action }
before do
visit admin_proposal_dashboard_actions_path
click_link 'Create'
end
it 'Creates a new action' do
fill_in 'proposal_dashboard_action_title', with: action.title
fill_in 'proposal_dashboard_action_description', with: action.description
click_button 'Save'
expect(page).to have_content(action.title)
end
end
context 'when editing an action' do
let!(:action) { create :proposal_dashboard_action }
let(:title) { Faker::Lorem.sentence }
before do
visit admin_proposal_dashboard_actions_path
click_link 'Edit'
end
it 'Updates the action' do
fill_in 'proposal_dashboard_action_title', with: title
click_button 'Save'
expect(page).to have_content(title)
end
end
context 'when destroying an action' do
let!(:action) { create :proposal_dashboard_action }
before do
visit admin_proposal_dashboard_actions_path
end
it 'deletes the action', js: true do
page.accept_confirm do
click_button 'Delete'
end
expect(page).not_to have_content(action.title)
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
describe Admin::ProposalDashboardActionsHelper do
describe 'active_human_readable' do
context 'when active is true' do
it 'returns label for active state' do
expect(active_human_readable(true)).to eq(t('admin.proposal_dashboard_actions.index.active'))
end
end
context 'when active is false' do
it 'returns label for inactive state' do
expect(active_human_readable(false)).to eq(t('admin.proposal_dashboard_actions.index.inactive'))
end
end
end
end

View File

@@ -87,4 +87,5 @@ describe Abilities::Administrator do
it { should be_able_to(:destroy, proposal_document) } it { should be_able_to(:destroy, proposal_document) }
it { should_not be_able_to(:destroy, budget_investment_image) } it { should_not be_able_to(:destroy, budget_investment_image) }
it { should_not be_able_to(:destroy, budget_investment_document) } it { should_not be_able_to(:destroy, budget_investment_document) }
it { should be_able_to(:manage, ProposalDashboardAction) }
end end

View File

@@ -104,6 +104,7 @@ describe Abilities::Common do
it { should be_able_to(:destroy, own_budget_investment_image) } it { should be_able_to(:destroy, own_budget_investment_image) }
it { should_not be_able_to(:destroy, budget_investment_image) } it { should_not be_able_to(:destroy, budget_investment_image) }
it { is_expected.not_to be_able_to(:manage, ProposalDashboardAction) }
describe 'flagging content' do describe 'flagging content' do
it { should be_able_to(:flag, debate) } it { should be_able_to(:flag, debate) }

View File

@@ -34,4 +34,5 @@ describe Abilities::Everyone do
it { should be_able_to(:read_results, finished_budget) } it { should be_able_to(:read_results, finished_budget) }
it { should_not be_able_to(:read_results, reviewing_ballot_budget) } it { should_not be_able_to(:read_results, reviewing_ballot_budget) }
it { is_expected.not_to be_able_to(:manage, ProposalDashboardAction) }
end end

View File

@@ -0,0 +1,130 @@
# frozen_string_literal: true
require 'rails_helper'
describe ProposalDashboardAction do
subject do
build :proposal_dashboard_action,
title: title,
description: description,
day_offset: day_offset,
required_supports: required_supports,
link: link,
request_to_administrators: request_to_administrators
end
let(:title) { Faker::Lorem.sentence }
let(:description) { Faker::Lorem.sentence }
let(:day_offset) { 0 }
let(:required_supports) { 0 }
let(:link) { nil }
let(:request_to_administrators) { true }
it { is_expected.to be_valid }
context 'when validating title' do
context 'and title is blank' do
let(:title) { nil }
it { is_expected.not_to be_valid }
end
context 'and title is very short' do
let(:title) { 'abc' }
it { is_expected.not_to be_valid }
end
context 'and title is very long' do
let(:title) { 'a' * 81 }
it { is_expected.not_to be_valid }
end
end
context 'when validating description' do
context 'and description is blank' do
let(:description) { nil }
it { is_expected.not_to be_valid }
end
context 'and description is very short' do
let(:description) { 'abc' }
it { is_expected.not_to be_valid }
end
context 'and description is very long' do
let(:description) { 'a' * 256 }
it { is_expected.not_to be_valid }
end
end
context 'when validating day_offset' do
context 'and day_offset is nil' do
let(:day_offset) { nil }
it { is_expected.not_to be_valid }
end
context 'and day_offset is negative' do
let(:day_offset) { -1 }
it { is_expected.not_to be_valid }
end
context 'and day_offset is not an integer' do
let(:day_offset) { 1.23 }
it { is_expected.not_to be_valid }
end
end
context 'when validating required_supports' do
context 'and required_supports is nil' do
let(:required_supports) { nil }
it { is_expected.not_to be_valid }
end
context 'and required_supports is negative' do
let(:required_supports) { -1 }
it { is_expected.not_to be_valid }
end
context 'and required_supports is not an integer' do
let(:required_supports) { 1.23 }
it { is_expected.not_to be_valid }
end
end
context 'when url is blank' do
let(:link) { nil }
context 'and no request_to_administrators' do
let(:request_to_administrators) { false }
it { is_expected.not_to be_valid }
end
context 'and request_to_administrators' do
let(:request_to_administrators) { true }
it { is_expected.to be_valid }
end
end
context 'when url is not blank' do
let(:link) { Faker::Internet.url }
context 'and no request_to_administrators' do
let(:request_to_administrators) { false }
it { is_expected.to be_valid }
end
end
end