Adds an entry inside moderation section that allows moderators to check
pending tasks and mark them as solved.
This commit is contained in:
Juan Salvador Pérez García
2018-06-18 11:39:04 +02:00
parent 83f78b1940
commit 33b3431c70
27 changed files with 384 additions and 9 deletions

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
class Moderation::AdministratorTasksController < Moderation::BaseController
helper_method :administrator_task
def index
authorize! :index, AdministratorTask
@administrator_tasks = AdministratorTask.pending
end
def edit
authorize! :edit, administrator_task
end
def update
authorize! :update, administrator_task
administrator_task.update(user: current_user, executed_at: Time.now)
redirect_to moderation_administrator_tasks_path, { flash: { notice: t('.success') } }
end
private
def administrator_task
@administrator_task ||= AdministratorTask.find(params[:id])
end
end

View File

@@ -4,7 +4,7 @@
class ProposalsDashboardController < ApplicationController class ProposalsDashboardController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
helper_method :proposal, :proposed_actions helper_method :proposal, :proposed_actions, :proposal_dashboard_action
respond_to :html respond_to :html
layout 'proposals_dashboard' layout 'proposals_dashboard'
@@ -21,12 +21,41 @@ class ProposalsDashboardController < ApplicationController
def execute def execute
authorize! :dashboard, proposal authorize! :dashboard, proposal
ProposalExecutedDashboardAction.create(proposal: proposal, proposal_dashboard_action: proposal_dashboard_action, executed_at: Time.now) ProposalExecutedDashboardAction.create(proposal: proposal, proposal_dashboard_action: proposal_dashboard_action, executed_at: Time.now)
redirect_to proposal_dashboard_index_path(proposal.to_param) redirect_to proposal_dashboard_index_path(proposal.to_param)
end end
def new_request
authorize! :dashboard, proposal
@proposal_executed_dashboard_action = ProposalExecutedDashboardAction.new
end
def create_request
authorize! :dashboard, proposal
source_params = proposal_executed_dashboard_action_params.merge(
proposal: proposal,
proposal_dashboard_action: proposal_dashboard_action,
executed_at: Time.now
)
@proposal_executed_dashboard_action = ProposalExecutedDashboardAction.new(source_params)
if @proposal_executed_dashboard_action.save
AdministratorTask.create(source: @proposal_executed_dashboard_action)
redirect_to proposal_dashboard_index_path(proposal.to_param), { flash: { info: t('.success') } }
else
render :new_request
end
end
private private
def proposal_executed_dashboard_action_params
params.require(:proposal_executed_dashboard_action).permit(:comments)
end
def proposal_dashboard_action def proposal_dashboard_action
@proposal_dashboard_action ||= ProposalDashboardAction.find(params[:id]) @proposal_dashboard_action ||= ProposalDashboardAction.find(params[:id])
end end

View File

@@ -63,6 +63,7 @@ module Abilities
cannot :moderate, ProposalNotification, author_id: user.id cannot :moderate, ProposalNotification, author_id: user.id
can :index, ProposalNotification can :index, ProposalNotification
can :manage, AdministratorTask
end end
end end
end end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
class AdministratorTask < ActiveRecord::Base
belongs_to :source, polymorphic: true
belongs_to :user
validates :source, presence: true
default_scope { order(created_at: :asc) }
scope :pending, -> { where(executed_at: nil) }
scope :done, -> { where.not(executed_at: nil) }
end

View File

@@ -59,4 +59,8 @@ class ProposalDashboardAction < ActiveRecord::Base
def request_to_administrators? def request_to_administrators?
request_to_administrators || false request_to_administrators || false
end end
def request_to_administrators?
request_to_administrators
end
end end

View File

@@ -4,7 +4,14 @@ class ProposalExecutedDashboardAction < ActiveRecord::Base
belongs_to :proposal belongs_to :proposal
belongs_to :proposal_dashboard_action belongs_to :proposal_dashboard_action
has_many :administrator_tasks, as: :source, dependent: :destroy
validates :proposal, presence: true, uniqueness: { scope: :proposal_dashboard_action } validates :proposal, presence: true, uniqueness: { scope: :proposal_dashboard_action }
validates :proposal_dashboard_action, presence: true validates :proposal_dashboard_action, presence: true
validates :executed_at, presence: true validates :executed_at, presence: true
validates :comments, presence: true, allow_blank: false, if: :comments_required?
def comments_required?
proposal_dashboard_action&.request_to_administrators? || false
end
end end

View File

@@ -42,5 +42,12 @@
<%= t("moderation.menu.users") %> <%= t("moderation.menu.users") %>
<% end %> <% end %>
</li> </li>
<li <%= 'class=is-active' if controller_name == 'administrator_tasks' %>>
<%= link_to moderation_administrator_tasks_path do %>
<span class="icon-check"></span>
<%= t 'moderation.menu.administrator_tasks' %>
<% end %>
</li>
</ul> </ul>
</nav> </nav>

View File

@@ -0,0 +1,15 @@
<%= form_for [:moderation, administrator_task] do |f| %>
<div class="callout">
<h5><%=t '.proposal', title: administrator_task.source.proposal.title %></h5>
<p><%=t '.request', title: administrator_task.source.proposal_dashboard_action.title %></p>
<%== administrator_task.source.comments unless administrator_task.source.comments.blank? %>
<%= link_to t('.check_details'), proposal_path(administrator_task.source.proposal), target: '_blank' %>
</div>
<div class="row">
<div class="actions small-12 large-3 medium-3 column">
<%= f.submit(class: 'button expanded', value: t('.solve')) %>
</div>
</div>
<% end %>

View File

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

View File

@@ -0,0 +1,34 @@
<h2 class="inline-block">
<%= AdministratorTask.model_name.human(count: 2) %>
</h2>
<table>
<thead>
<tr>
<th><%= AdministratorTask.human_attribute_name(:source) %></th>
<th></th>
</tr>
</thead>
<tbody>
<% if @administrator_tasks.empty? %>
<tr>
<td colspan="100%"><%= t '.no_records' %></td>
</tr>
<% end %>
<% @administrator_tasks.each do |task| %>
<tr id="<%= dom_id(task) %>">
<td>
<%= task.source.proposal.title %>:
<%= task.source.proposal_dashboard_action.title %>
</td>
<td style="text-align: right">
<%= link_to t('.solve'),
edit_moderation_administrator_task_path(task),
class: 'edit-banner button hollow' %>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,12 @@
<%= form_for @proposal_executed_dashboard_action,
url: create_request_proposal_dashboard_url(proposal, proposal_dashboard_action) do |f| %>
<%= render 'shared/errors', resource: @proposal_executed_dashboard_action %>
<div class="ckeditor small-12 column">
<%= f.label :comments %>
<%= f.cktext_area :comments, ckeditor: { language: I18n.locale }, label: false %>
</div>
<div class="actions small-12 column">
<%= f.submit(class: 'button', value: t('.request')) %>
</div>
<% end %>

View File

@@ -16,10 +16,15 @@
<% if action.proposals.where(id: proposal.id).any? %> <% if action.proposals.where(id: proposal.id).any? %>
<%=l action.proposal_executed_dashboard_actions.find_by(proposal: proposal).executed_at, format: :short %> <%=l action.proposal_executed_dashboard_actions.find_by(proposal: proposal).executed_at, format: :short %>
<% else %> <% else %>
<%= link_to t('.execute'), <% if action.request_to_administrators? %>
execute_proposal_dashboard_path(proposal.to_param, action.to_param), <%= link_to t('.execute'),
method: 'post', new_request_proposal_dashboard_path(proposal, action) %>
data: { confirm: t('admin.actions.confirm') } %> <% else %>
<%= link_to t('.execute'),
execute_proposal_dashboard_path(proposal.to_param, action.to_param),
method: 'post',
data: { confirm: t('admin.actions.confirm') } %>
<% end %>
<% end %> <% end %>
</td> </td>
</tr> </tr>

View File

@@ -0,0 +1,13 @@
<div class="proposals-dashboard-new-request-form row">
<div class="small-12 medium-9 column">
<%= back_link_to %>
<h1><%= proposal_dashboard_action.title %></h1>
<div data-alert class="callout primary">
<%= proposal_dashboard_action.description %>
</div>
<%= render 'proposals_dashboard/form' %>
</div>
</div>

View File

@@ -109,6 +109,9 @@ en:
proposal_dashboard_action: proposal_dashboard_action:
one: Proposal dashboard action one: Proposal dashboard action
other: Proposal dashboard actions other: Proposal dashboard actions
administrator_task:
one: Task
other: Tasks
attributes: attributes:
budget: budget:
name: "Name" name: "Name"
@@ -272,6 +275,12 @@ en:
order: Order order: Order
active: Active active: Active
action_type: Type action_type: Type
proposal_executed_dashboard_action:
comments: Comments for the administrator
administrator_task:
source: Source
user: Executed by
executed_at: Executed at
errors: errors:
models: models:
user: user:

View File

@@ -502,6 +502,10 @@ en:
actions: Actions actions: Actions
proposed_action: proposed_action:
execute: Execute execute: Execute
form:
request: Request
create_request:
success: The request for the administrator has been successfully sent.
polls: polls:
all: "All" all: "All"
no_dates: "no date assigned" no_dates: "no date assigned"

View File

@@ -49,6 +49,7 @@ en:
proposals: Proposals proposals: Proposals
proposal_notifications: Proposals notifications proposal_notifications: Proposals notifications
users: Block users users: Block users
administrator_tasks: Pending tasks
proposals: proposals:
index: index:
block_authors: Block authors block_authors: Block authors
@@ -95,3 +96,17 @@ en:
search_placeholder: email or name of user search_placeholder: email or name of user
title: Block users title: Block users
notice_hide: User blocked. All of this user's debates and comments have been hidden. notice_hide: User blocked. All of this user's debates and comments have been hidden.
administrator_tasks:
index:
solve: Solve
no_records: There are no pending tasks
edit:
back: Back to pending tasks list
solving: Solve pending task
form:
solve: Mark as solved
proposal: "The proposal: %{title}"
request: "Has requested: %{title}"
check_details: Check the proposal details
update:
success: The task has been marked as solved.

View File

@@ -109,6 +109,9 @@ es:
proposal_dashboard_action: proposal_dashboard_action:
one: Acción del panel de control de propuestas one: Acción del panel de control de propuestas
other: Acciones del panel de control de propuestas other: Acciones del panel de control de propuestas
administrator_task:
one: Tarea
other: Tareas
attributes: attributes:
budget: budget:
name: "Nombre" name: "Nombre"
@@ -273,6 +276,12 @@ es:
order: Orden order: Orden
active: Activa active: Activa
action_type: Tipo action_type: Tipo
proposal_executed_dashboard_action:
comments: Comentarios para el administrador
administrator_task:
source: Fuente
user: Ejecutado por
executed_at: Ejecutado el
errors: errors:
models: models:
user: user:

View File

@@ -502,6 +502,10 @@ es:
actions: Acciones actions: Acciones
proposed_action: proposed_action:
execute: Ejecutar execute: Ejecutar
form:
request: Solicitar
create_request:
success: La petición ha sido correctamente enviada al administrador.
polls: polls:
all: "Todas" all: "Todas"
no_dates: "sin fecha asignada" no_dates: "sin fecha asignada"

View File

@@ -49,6 +49,7 @@ es:
proposals: Propuestas proposals: Propuestas
proposal_notifications: Notificaciones de propuestas proposal_notifications: Notificaciones de propuestas
users: Bloquear usuarios users: Bloquear usuarios
administrator_tasks: Tareas pendientes
proposals: proposals:
index: index:
block_authors: Bloquear autores block_authors: Bloquear autores
@@ -95,3 +96,17 @@ es:
search_placeholder: email o nombre de usuario search_placeholder: email o nombre de usuario
title: Bloquear usuarios title: Bloquear usuarios
notice_hide: Usuario bloqueado. Se han ocultado todos sus debates y comentarios. notice_hide: Usuario bloqueado. Se han ocultado todos sus debates y comentarios.
administrator_tasks:
index:
solve: Resolver
no_records: No hay tareas pendientes
edit:
back: Volver a la lista de tareas pendientes
solving: Resolver tarea pendiente
form:
solve: Marcar como resuelta
proposal: "La propuesta: %{title}"
request: "Ha solicitado: %{title}"
check_details: Ver los detalles de la propuesta
update:
success: La tarea ha sido marcada como resuelta

View File

@@ -27,4 +27,6 @@ namespace :moderation do
put :hide, on: :member put :hide, on: :member
put :moderate, on: :collection put :moderate, on: :collection
end end
resources :administrator_tasks, only: %i[index edit update]
end end

View File

@@ -6,6 +6,8 @@ resources :proposals do
member do member do
post :execute post :execute
get :new_request
post :create_request
end end
end end

View File

@@ -0,0 +1,11 @@
class CreateAdministratorTasks < ActiveRecord::Migration
def change
create_table :administrator_tasks do |t|
t.references :source, polymorphic: true, index: true
t.references :user, index: true, foreign_key: true
t.datetime :executed_at
t.timestamps null: false
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: 20180711224810) do ActiveRecord::Schema.define(version: 20180615102215) 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"
@@ -30,6 +30,18 @@ ActiveRecord::Schema.define(version: 20180711224810) do
add_index "activities", ["actionable_id", "actionable_type"], name: "index_activities_on_actionable_id_and_actionable_type", using: :btree add_index "activities", ["actionable_id", "actionable_type"], name: "index_activities_on_actionable_id_and_actionable_type", using: :btree
add_index "activities", ["user_id"], name: "index_activities_on_user_id", using: :btree add_index "activities", ["user_id"], name: "index_activities_on_user_id", using: :btree
create_table "administrator_tasks", force: :cascade do |t|
t.integer "source_id"
t.string "source_type"
t.integer "user_id"
t.datetime "executed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "administrator_tasks", ["source_type", "source_id"], name: "index_administrator_tasks_on_source_type_and_source_id", using: :btree
add_index "administrator_tasks", ["user_id"], name: "index_administrator_tasks_on_user_id", using: :btree
create_table "administrators", force: :cascade do |t| create_table "administrators", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
end end
@@ -1301,6 +1313,7 @@ ActiveRecord::Schema.define(version: 20180711224810) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_foreign_key "administrator_tasks", "users"
add_foreign_key "administrators", "users" add_foreign_key "administrators", "users"
add_foreign_key "annotations", "legacy_legislations" add_foreign_key "annotations", "legacy_legislations"
add_foreign_key "annotations", "users" add_foreign_key "annotations", "users"

View File

@@ -1081,4 +1081,20 @@ LOREM_IPSUM
comments { Faker::Lorem.sentence(10) } comments { Faker::Lorem.sentence(10) }
end end
end end
factory :administrator_task do
source { |s| s.association(:proposal_executed_dashboard_action, :with_comments) }
user
executed_at { Time.now }
trait :pending do
user { nil }
executed_at { nil }
end
trait :done do
user
executed_at { Time.now }
end
end
end end

View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Administrator tasks moderation' do
let(:moderator) { create(:moderator) }
before do
login_as moderator.user
end
context 'when accessing the pending task list' do
context 'and no pending task' do
before do
visit moderation_administrator_tasks_path
end
it 'informs that there are no pending tasks' do
expect(page).to have_content('There are no pending tasks')
end
end
context 'and there are pending tasks' do
let!(:task) { create :administrator_task, :pending }
before do
visit moderation_administrator_tasks_path
end
it 'shows the related proposal title' do
expect(page).to have_content(task.source.proposal.title)
end
it 'shows the requested action title' do
expect(page).to have_content(task.source.proposal_dashboard_action.title)
end
it 'has a link that allows solving the request' do
expect(page).to have_link('Solve')
end
end
end
context 'when solving a pending task' do
let!(:task) { create :administrator_task, :pending }
before do
visit moderation_administrator_tasks_path
click_link 'Solve'
end
it 'contains a link to the proposal' do
expect(page).to have_link('Check the proposal details')
end
it 'contains a button that solves the request' do
expect(page).to have_button('Mark as solved')
end
it 'shows the comments added by the user during the request' do
expect(page).to have_content(task.source.comments)
end
context 'and the Mark as solved button is pressed' do
before do
click_button 'Mark as solved'
end
it 'The proposal dissapears from the list' do
expect(page).not_to have_content(task.source.proposal.title)
end
end
end
end

View File

@@ -21,6 +21,7 @@ describe Abilities::Moderator do
let(:hidden_debate) { create(:debate, :hidden) } let(:hidden_debate) { create(:debate, :hidden) }
let(:hidden_comment) { create(:comment, :hidden) } let(:hidden_comment) { create(:comment, :hidden) }
let(:hidden_proposal) { create(:proposal, :hidden) } let(:hidden_proposal) { create(:proposal, :hidden) }
let(:administrator_task) { create(:administrator_task) }
it { should be_able_to(:index, Debate) } it { should be_able_to(:index, Debate) }
it { should be_able_to(:show, debate) } it { should be_able_to(:show, debate) }
@@ -31,6 +32,9 @@ describe Abilities::Moderator do
it { should be_able_to(:read, Organization) } it { should be_able_to(:read, Organization) }
it { is_expected.to be_able_to :manage, AdministratorTask }
it { is_expected.to be_able_to :manage, administrator_task }
describe "organizations" do describe "organizations" do
let(:pending_organization) { create(:organization) } let(:pending_organization) { create(:organization) }
let(:rejected_organization) { create(:organization, :rejected) } let(:rejected_organization) { create(:organization, :rejected) }

View File

@@ -6,12 +6,17 @@ describe ProposalExecutedDashboardAction do
build :proposal_executed_dashboard_action, build :proposal_executed_dashboard_action,
proposal: proposal, proposal: proposal,
proposal_dashboard_action: proposal_dashboard_action, proposal_dashboard_action: proposal_dashboard_action,
executed_at: executed_at executed_at: executed_at,
comments: comments
end end
let(:proposal) { create :proposal } let(:proposal) { create :proposal }
let(:proposal_dashboard_action) { create :proposal_dashboard_action } let(:proposal_dashboard_action) do
create :proposal_dashboard_action, request_to_administrators: request_to_administrators, link: Faker::Internet.url
end
let(:request_to_administrators) { false }
let(:executed_at) { Time.now } let(:executed_at) { Time.now }
let(:comments) { '' }
it { is_expected.to be_valid } it { is_expected.to be_valid }
@@ -33,6 +38,22 @@ describe ProposalExecutedDashboardAction do
it { is_expected.not_to be_valid } it { is_expected.not_to be_valid }
end end
context 'when the action sends a request to the administrators' do
let(:request_to_administrators) { true }
context 'and comments are blank' do
let(:comments) { '' }
it { is_expected.not_to be_valid }
end
context 'and comments have value' do
let(:comments) { Faker::Lorem.sentence }
it { is_expected.to be_valid }
end
end
context 'when it has been already executed' do context 'when it has been already executed' do
let!(:executed) { create(:proposal_executed_dashboard_action, proposal: proposal, proposal_dashboard_action: proposal_dashboard_action) } let!(:executed) { create(:proposal_executed_dashboard_action, proposal: proposal, proposal_dashboard_action: proposal_dashboard_action) }