diff --git a/app/controllers/moderation/administrator_tasks_controller.rb b/app/controllers/moderation/administrator_tasks_controller.rb new file mode 100644 index 000000000..efdb727f8 --- /dev/null +++ b/app/controllers/moderation/administrator_tasks_controller.rb @@ -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 diff --git a/app/controllers/proposals_dashboard_controller.rb b/app/controllers/proposals_dashboard_controller.rb index 0e235664c..73c7f4778 100644 --- a/app/controllers/proposals_dashboard_controller.rb +++ b/app/controllers/proposals_dashboard_controller.rb @@ -4,7 +4,7 @@ class ProposalsDashboardController < ApplicationController before_action :authenticate_user! - helper_method :proposal, :proposed_actions + helper_method :proposal, :proposed_actions, :proposal_dashboard_action respond_to :html layout 'proposals_dashboard' @@ -21,12 +21,41 @@ class ProposalsDashboardController < ApplicationController def execute authorize! :dashboard, proposal + ProposalExecutedDashboardAction.create(proposal: proposal, proposal_dashboard_action: proposal_dashboard_action, executed_at: Time.now) redirect_to proposal_dashboard_index_path(proposal.to_param) 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 + def proposal_executed_dashboard_action_params + params.require(:proposal_executed_dashboard_action).permit(:comments) + end + def proposal_dashboard_action @proposal_dashboard_action ||= ProposalDashboardAction.find(params[:id]) end diff --git a/app/models/abilities/moderation.rb b/app/models/abilities/moderation.rb index 801e752ed..1f26ff00a 100644 --- a/app/models/abilities/moderation.rb +++ b/app/models/abilities/moderation.rb @@ -63,6 +63,7 @@ module Abilities cannot :moderate, ProposalNotification, author_id: user.id can :index, ProposalNotification + can :manage, AdministratorTask end end end diff --git a/app/models/administrator_task.rb b/app/models/administrator_task.rb new file mode 100644 index 000000000..0bb085bb9 --- /dev/null +++ b/app/models/administrator_task.rb @@ -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 diff --git a/app/models/proposal_dashboard_action.rb b/app/models/proposal_dashboard_action.rb index e438d9f76..42af2119f 100644 --- a/app/models/proposal_dashboard_action.rb +++ b/app/models/proposal_dashboard_action.rb @@ -59,4 +59,8 @@ class ProposalDashboardAction < ActiveRecord::Base def request_to_administrators? request_to_administrators || false end + + def request_to_administrators? + request_to_administrators + end end diff --git a/app/models/proposal_executed_dashboard_action.rb b/app/models/proposal_executed_dashboard_action.rb index 6d205ac90..cf0b0fca6 100644 --- a/app/models/proposal_executed_dashboard_action.rb +++ b/app/models/proposal_executed_dashboard_action.rb @@ -4,7 +4,14 @@ class ProposalExecutedDashboardAction < ActiveRecord::Base belongs_to :proposal belongs_to :proposal_dashboard_action + has_many :administrator_tasks, as: :source, dependent: :destroy + validates :proposal, presence: true, uniqueness: { scope: :proposal_dashboard_action } validates :proposal_dashboard_action, 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 diff --git a/app/views/moderation/_menu.html.erb b/app/views/moderation/_menu.html.erb index 8d7abb85d..e13c74b2b 100644 --- a/app/views/moderation/_menu.html.erb +++ b/app/views/moderation/_menu.html.erb @@ -42,5 +42,12 @@ <%= t("moderation.menu.users") %> <% end %> + +
  • > + <%= link_to moderation_administrator_tasks_path do %> + + <%= t 'moderation.menu.administrator_tasks' %> + <% end %> +
  • diff --git a/app/views/moderation/administrator_tasks/_form.html.erb b/app/views/moderation/administrator_tasks/_form.html.erb new file mode 100644 index 000000000..a5bd8ca81 --- /dev/null +++ b/app/views/moderation/administrator_tasks/_form.html.erb @@ -0,0 +1,15 @@ +<%= form_for [:moderation, administrator_task] do |f| %> + +
    +
    <%=t '.proposal', title: administrator_task.source.proposal.title %>
    +

    <%=t '.request', title: administrator_task.source.proposal_dashboard_action.title %>

    + <%== administrator_task.source.comments unless administrator_task.source.comments.blank? %> + <%= link_to t('.check_details'), proposal_path(administrator_task.source.proposal), target: '_blank' %> +
    + +
    +
    + <%= f.submit(class: 'button expanded', value: t('.solve')) %> +
    +
    +<% end %> diff --git a/app/views/moderation/administrator_tasks/edit.html.erb b/app/views/moderation/administrator_tasks/edit.html.erb new file mode 100644 index 000000000..155cae8c2 --- /dev/null +++ b/app/views/moderation/administrator_tasks/edit.html.erb @@ -0,0 +1,9 @@ +
    +
    + <%= back_link_to moderation_administrator_tasks_path, t('.back') %> + +

    <%= t('.solving') %>

    + + <%= render 'form' %> +
    +
    diff --git a/app/views/moderation/administrator_tasks/index.html.erb b/app/views/moderation/administrator_tasks/index.html.erb new file mode 100644 index 000000000..73ad994cc --- /dev/null +++ b/app/views/moderation/administrator_tasks/index.html.erb @@ -0,0 +1,34 @@ +

    + <%= AdministratorTask.model_name.human(count: 2) %> +

    + + + + + + + + + + + <% if @administrator_tasks.empty? %> + + + + <% end %> + + <% @administrator_tasks.each do |task| %> + + + + + <% end %> + +
    <%= AdministratorTask.human_attribute_name(:source) %>
    <%= t '.no_records' %>
    + <%= task.source.proposal.title %>: + <%= task.source.proposal_dashboard_action.title %> + + <%= link_to t('.solve'), + edit_moderation_administrator_task_path(task), + class: 'edit-banner button hollow' %> +
    diff --git a/app/views/proposals_dashboard/_form.html.erb b/app/views/proposals_dashboard/_form.html.erb new file mode 100644 index 000000000..c4bebac28 --- /dev/null +++ b/app/views/proposals_dashboard/_form.html.erb @@ -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 %> + +
    + <%= f.label :comments %> + <%= f.cktext_area :comments, ckeditor: { language: I18n.locale }, label: false %> +
    +
    + <%= f.submit(class: 'button', value: t('.request')) %> +
    +<% end %> diff --git a/app/views/proposals_dashboard/_proposed_action.html.erb b/app/views/proposals_dashboard/_proposed_action.html.erb index 237ad83bf..b91a6c736 100644 --- a/app/views/proposals_dashboard/_proposed_action.html.erb +++ b/app/views/proposals_dashboard/_proposed_action.html.erb @@ -16,10 +16,15 @@ <% if action.proposals.where(id: proposal.id).any? %> <%=l action.proposal_executed_dashboard_actions.find_by(proposal: proposal).executed_at, format: :short %> <% else %> - <%= link_to t('.execute'), - execute_proposal_dashboard_path(proposal.to_param, action.to_param), - method: 'post', - data: { confirm: t('admin.actions.confirm') } %> + <% if action.request_to_administrators? %> + <%= link_to t('.execute'), + new_request_proposal_dashboard_path(proposal, action) %> + <% 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 %> diff --git a/app/views/proposals_dashboard/new_request.html.erb b/app/views/proposals_dashboard/new_request.html.erb new file mode 100644 index 000000000..1290e4699 --- /dev/null +++ b/app/views/proposals_dashboard/new_request.html.erb @@ -0,0 +1,13 @@ +
    + +
    + <%= back_link_to %> + +

    <%= proposal_dashboard_action.title %>

    +
    + <%= proposal_dashboard_action.description %> +
    + + <%= render 'proposals_dashboard/form' %> +
    +
    diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index d305d3bbf..f2454734c 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -109,6 +109,9 @@ en: proposal_dashboard_action: one: Proposal dashboard action other: Proposal dashboard actions + administrator_task: + one: Task + other: Tasks attributes: budget: name: "Name" @@ -272,6 +275,12 @@ en: order: Order active: Active action_type: Type + proposal_executed_dashboard_action: + comments: Comments for the administrator + administrator_task: + source: Source + user: Executed by + executed_at: Executed at errors: models: user: diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 216ad41cc..c836d6ed1 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -502,6 +502,10 @@ en: actions: Actions proposed_action: execute: Execute + form: + request: Request + create_request: + success: The request for the administrator has been successfully sent. polls: all: "All" no_dates: "no date assigned" diff --git a/config/locales/en/moderation.yml b/config/locales/en/moderation.yml index 5ed2f69bb..79494f94c 100644 --- a/config/locales/en/moderation.yml +++ b/config/locales/en/moderation.yml @@ -49,6 +49,7 @@ en: proposals: Proposals proposal_notifications: Proposals notifications users: Block users + administrator_tasks: Pending tasks proposals: index: block_authors: Block authors @@ -94,4 +95,18 @@ en: search: Search search_placeholder: email or name of user title: Block users - notice_hide: User blocked. All of this user's debates and comments have been hidden. \ No newline at end of file + 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. diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index cd9376892..0b17a6f4c 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -109,6 +109,9 @@ es: proposal_dashboard_action: one: Acción del panel de control de propuestas other: Acciones del panel de control de propuestas + administrator_task: + one: Tarea + other: Tareas attributes: budget: name: "Nombre" @@ -273,6 +276,12 @@ es: order: Orden active: Activa action_type: Tipo + proposal_executed_dashboard_action: + comments: Comentarios para el administrador + administrator_task: + source: Fuente + user: Ejecutado por + executed_at: Ejecutado el errors: models: user: diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 29f2bc68d..76c8ca2da 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -502,6 +502,10 @@ es: actions: Acciones proposed_action: execute: Ejecutar + form: + request: Solicitar + create_request: + success: La petición ha sido correctamente enviada al administrador. polls: all: "Todas" no_dates: "sin fecha asignada" diff --git a/config/locales/es/moderation.yml b/config/locales/es/moderation.yml index c51013aee..289d06018 100644 --- a/config/locales/es/moderation.yml +++ b/config/locales/es/moderation.yml @@ -49,6 +49,7 @@ es: proposals: Propuestas proposal_notifications: Notificaciones de propuestas users: Bloquear usuarios + administrator_tasks: Tareas pendientes proposals: index: block_authors: Bloquear autores @@ -95,3 +96,17 @@ es: search_placeholder: email o nombre de usuario title: Bloquear usuarios 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 diff --git a/config/routes/moderation.rb b/config/routes/moderation.rb index 90dc9be27..1f354ad4a 100644 --- a/config/routes/moderation.rb +++ b/config/routes/moderation.rb @@ -27,4 +27,6 @@ namespace :moderation do put :hide, on: :member put :moderate, on: :collection end + + resources :administrator_tasks, only: %i[index edit update] end diff --git a/config/routes/proposal.rb b/config/routes/proposal.rb index 9666d77f7..b7db55c2d 100644 --- a/config/routes/proposal.rb +++ b/config/routes/proposal.rb @@ -6,6 +6,8 @@ resources :proposals do member do post :execute + get :new_request + post :create_request end end diff --git a/db/migrate/20180615102215_create_administrator_tasks.rb b/db/migrate/20180615102215_create_administrator_tasks.rb new file mode 100644 index 000000000..08bda8cf4 --- /dev/null +++ b/db/migrate/20180615102215_create_administrator_tasks.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 031c55128..8550390c4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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 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", ["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| t.integer "user_id" end @@ -1301,6 +1313,7 @@ ActiveRecord::Schema.define(version: 20180711224810) do t.datetime "updated_at", null: false end + add_foreign_key "administrator_tasks", "users" add_foreign_key "administrators", "users" add_foreign_key "annotations", "legacy_legislations" add_foreign_key "annotations", "users" diff --git a/spec/factories.rb b/spec/factories.rb index 2012f8008..2bd9e958c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1081,4 +1081,20 @@ LOREM_IPSUM comments { Faker::Lorem.sentence(10) } 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 diff --git a/spec/features/moderation/administrator_tasks_spec.rb b/spec/features/moderation/administrator_tasks_spec.rb new file mode 100644 index 000000000..148967b44 --- /dev/null +++ b/spec/features/moderation/administrator_tasks_spec.rb @@ -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 diff --git a/spec/models/abilities/moderator_spec.rb b/spec/models/abilities/moderator_spec.rb index a2b374a85..9088e4468 100644 --- a/spec/models/abilities/moderator_spec.rb +++ b/spec/models/abilities/moderator_spec.rb @@ -21,6 +21,7 @@ describe Abilities::Moderator do let(:hidden_debate) { create(:debate, :hidden) } let(:hidden_comment) { create(:comment, :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(:show, debate) } @@ -31,6 +32,9 @@ describe Abilities::Moderator do 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 let(:pending_organization) { create(:organization) } let(:rejected_organization) { create(:organization, :rejected) } diff --git a/spec/models/proposal_executed_dashboard_action_spec.rb b/spec/models/proposal_executed_dashboard_action_spec.rb index bc76d6800..dc7a4bc21 100644 --- a/spec/models/proposal_executed_dashboard_action_spec.rb +++ b/spec/models/proposal_executed_dashboard_action_spec.rb @@ -6,12 +6,17 @@ describe ProposalExecutedDashboardAction do build :proposal_executed_dashboard_action, proposal: proposal, proposal_dashboard_action: proposal_dashboard_action, - executed_at: executed_at + executed_at: executed_at, + comments: comments end 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(:comments) { '' } it { is_expected.to be_valid } @@ -33,6 +38,22 @@ describe ProposalExecutedDashboardAction do it { is_expected.not_to be_valid } 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 let!(:executed) { create(:proposal_executed_dashboard_action, proposal: proposal, proposal_dashboard_action: proposal_dashboard_action) }