diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb new file mode 100644 index 000000000..9b23b7927 --- /dev/null +++ b/app/controllers/admin/users_controller.rb @@ -0,0 +1,25 @@ +class Admin::UsersController < Admin::BaseController + + def index + @users = User.only_hidden.page(params[:page]) + end + + def show + @user = User.with_hidden.find(params[:id]) + @debates = Debate.where(author_id: @user.id).with_hidden.page(params[:page]) + @comments = Comment.where(user_id: @user.id).with_hidden.page(params[:page]) + end + + def restore + user = User.with_hidden.find(params[:id]) + if hidden_at = user.hidden_at + debates_ids = Debate.only_hidden.where(author_id: user.id).where("debates.hidden_at > ?", hidden_at).pluck(:id) + comments_ids = Comment.only_hidden.where(user_id: user.id).where("comments.hidden_at > ?", hidden_at).pluck(:id) + + user.restore + Debate.restore_all debates_ids + Comment.restore_all comments_ids + end + redirect_to admin_users_path, notice: t('admin.users.restore.success') + end +end \ No newline at end of file diff --git a/app/controllers/moderation/debates_controller.rb b/app/controllers/moderation/debates_controller.rb index cb5599092..622116765 100644 --- a/app/controllers/moderation/debates_controller.rb +++ b/app/controllers/moderation/debates_controller.rb @@ -4,4 +4,5 @@ class Moderation::DebatesController < Moderation::BaseController @debate = Debate.find(params[:id]) @debate.hide end + end \ No newline at end of file diff --git a/app/controllers/moderation/users_controller.rb b/app/controllers/moderation/users_controller.rb new file mode 100644 index 000000000..5dc2178a6 --- /dev/null +++ b/app/controllers/moderation/users_controller.rb @@ -0,0 +1,15 @@ +class Moderation::UsersController < Moderation::BaseController + + def hide + user = User.find(params[:id]) + debates_ids = Debate.where(author_id: user.id).pluck(:id) + comments_ids = Comment.where(user_id: user.id).pluck(:id) + + user.hide + Debate.hide_all debates_ids + Comment.hide_all comments_ids + + redirect_to debates_path + end + +end \ No newline at end of file diff --git a/app/models/ability.rb b/app/models/ability.rb index 9cce129a2..10a136e22 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -28,11 +28,13 @@ class Ability can :hide, Comment can :hide, Debate + can :hide, User end if user.administrator? can :restore, Comment can :restore, Debate + can :restore, User end end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 0141c1ffa..0c298182d 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -9,7 +9,7 @@ class Comment < ActiveRecord::Base validates :user, presence: true belongs_to :commentable, polymorphic: true - belongs_to :user + belongs_to :user, -> { with_hidden } default_scope { includes(:user) } scope :recent, -> { order(id: :desc) } @@ -36,6 +36,10 @@ class Comment < ActiveRecord::Base votes_for.size end + def not_visible? + hidden? || user.hidden? + end + # TODO: faking counter cache since there is a bug with acts_as_nested_set :counter_cache # Remove when https://github.com/collectiveidea/awesome_nested_set/issues/294 is fixed # and reset counters using diff --git a/app/models/debate.rb b/app/models/debate.rb index bd7936e73..4d9b4ffe8 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -11,7 +11,7 @@ class Debate < ActiveRecord::Base acts_as_taggable acts_as_paranoid column: :hidden_at - belongs_to :author, class_name: 'User', foreign_key: 'author_id' + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' validates :title, presence: true validates :description, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index b6374f518..52287d9f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,11 @@ class User < ActiveRecord::Base + include ActsAsParanoidAliases apply_simple_captcha devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable acts_as_voter + acts_as_paranoid column: :hidden_at has_one :administrator has_one :moderator diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 06cdac5ca..bc2d459e2 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -25,6 +25,13 @@ <% end %> +
  • > + <%= link_to admin_users_path do %> + + <%= t('admin.menu.hidden_users') %> + <% end %> +
  • +
  • > <%= link_to admin_organizations_path do %> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb new file mode 100644 index 000000000..5c84468f8 --- /dev/null +++ b/app/views/admin/users/index.html.erb @@ -0,0 +1,18 @@ +

    <%= t("admin.users.index.title") %>

    + +

    <%= page_entries_info @users %>

    + + + +<%= paginate @users %> + + diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb new file mode 100644 index 000000000..375d9110e --- /dev/null +++ b/app/views/admin/users/show.html.erb @@ -0,0 +1,43 @@ +

    <%= t("admin.users.show.title", user: @user.name) %>

    + +

    + <%= t("admin.users.show.email") %> <%= @user.email %> | + <%= t("admin.users.show.registered_at") %> <%= @user.confirmed_at %> | + <%= t("admin.users.show.hidden_at") %> <%= @user.hidden_at %> +

    +

    + <%= link_to t("admin.users.show.restore"), restore_admin_user_path(@user), + method: :put, data: { confirm: t('admin.actions.confirm') }, class: "button radius tiny" %> + <%= link_to t("admin.users.show.back"), admin_users_path, + class: "button radius tiny secondary" %> +

    + +<% if @debates.present? %> +

    <%= page_entries_info @debates %>

    +<% end %> + + + +<% if @comments.present? %> +

    <%= page_entries_info @comments %>

    +<% end %> + + + +<%= paginate [@debates, @comments].sort_by {|x| x.size}.last %> diff --git a/app/views/comments/_actions.html.erb b/app/views/comments/_actions.html.erb index ecc9bc9c0..8cc1b114f 100644 --- a/app/views/comments/_actions.html.erb +++ b/app/views/comments/_actions.html.erb @@ -1,5 +1,10 @@  |  <%= link_to t("admin.actions.hide").capitalize, hide_moderation_comment_path(comment), - method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %> + method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %> + <% unless comment.user.hidden? %> +  |  + <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(comment.user_id, debate_id: @debate.id), + method: :put, data: { confirm: t('admin.actions.confirm') } %> + <% end %> \ No newline at end of file diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index d0317d52f..ea6912094 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,21 +1,21 @@
    - <% if comment.hidden? %> + <% if comment.not_visible? %> <%= t("debates.comment.deleted") %> <% else %> <%= avatar_image(comment.user, size: 32, class: 'left') %> - + <% end %>
    - + <% else %> <%= comment.user.name %> <% if comment.user.official? %>  •  @@ -23,7 +23,7 @@ <%= comment.user.official_position %> <% end %> - + <% end %> <% if comment.user.verified_organization? %>  •  @@ -70,7 +70,7 @@ <% end %>
    - <%= render comment.children.with_deleted.reorder('id DESC, lft') %> + <%= render comment.children.with_hidden.reorder('id DESC, lft') %>
    diff --git a/app/views/debates/_actions.html.erb b/app/views/debates/_actions.html.erb index 3300a7b1d..5a51749dc 100644 --- a/app/views/debates/_actions.html.erb +++ b/app/views/debates/_actions.html.erb @@ -1,2 +1,8 @@ <%= link_to t("admin.actions.hide").capitalize, hide_moderation_debate_path(debate), method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %> + +<% unless debate.author.hidden? %> +  |  + <%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(debate.author_id), + method: :put, data: { confirm: t('admin.actions.confirm') } %> +<% end %> diff --git a/app/views/debates/show.html.erb b/app/views/debates/show.html.erb index 3a29ec96f..03c7e2c8c 100644 --- a/app/views/debates/show.html.erb +++ b/app/views/debates/show.html.erb @@ -14,12 +14,12 @@
    <%= avatar_image(@debate.author, size: 32, class: 'author-photo') %> - + <% else %> <%= @debate.author.name %> @@ -29,7 +29,7 @@ <%= @debate.author.official_position %> <% end %> - + <% end %> <% if @debate.author.verified_organization? %>  •  diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index 7a3c6969b..151a9fc47 100644 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -13,6 +13,7 @@ en: debate_topics: Debate topics hidden_debates: Hidden debates hidden_comments: Hidden comments + hidden_users: Hidden users organizations: Organizations officials: Officials stats: Statistics @@ -31,6 +32,7 @@ en: rejected: Rejected actions: hide: Hide + hide_author: Ban author restore: Restore confirm: 'Are you sure?' tags: @@ -53,6 +55,19 @@ en: back: Back restore: success: The debate has been restored + users: + index: + title: Banned users + restore: Restore user + show: + title: "User activity from %{user}" + restore: Restore user + back: Back + email: "Email:" + registered_at: "Registered at:" + hidden_at: "Hidden at:" + restore: + success: The user has been restored officials: level_0: Level 0 level_1: Level 1 diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index da29cff28..fbaed7566 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -13,6 +13,7 @@ es: debate_topics: Temas de debate hidden_debates: Debates ocultos hidden_comments: Comentarios ocultos + hidden_users: Usuarios ocultos organizations: Organizaciones officials: Cargos públicos stats: Estadísticas @@ -31,6 +32,7 @@ es: rejected: Rechazadas actions: hide: Ocultar + hide_author: Bloquear al autor restore: Permitir confirm: '¿Estás seguro?' tags: @@ -53,6 +55,19 @@ es: back: Volver restore: success: El debate ha sido permitido + users: + index: + title: Usuarios bloqueados + restore: Restaurar usuario + show: + title: "Actividad del usuario %{user}" + restore: Restaurar usuario + back: Volver + email: "Email:" + registered_at: "Fecha de alta:" + hidden_at: "Bloqueado:" + restore: + success: El usuario y sus contenidos han sido restaurados officials: level_0: Nivel 0 level_1: Nivel 1 diff --git a/config/routes.rb b/config/routes.rb index 66b74546b..7dbe31613 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,6 +36,10 @@ Rails.application.routes.draw do end end + resources :users, only: [:index, :show] do + member { put :restore } + end + resources :debates, only: [:index, :show] do member { put :restore } end @@ -55,11 +59,15 @@ Rails.application.routes.draw do namespace :moderation do root to: "dashboard#index" + resources :users, only: [] do + member { put :hide } + end + resources :debates, only: [] do member { put :hide } end - resources :comments, only: [:index] do + resources :comments, only: [] do member { put :hide } end end diff --git a/db/migrate/20150819135933_add_hidden_at_to_users.rb b/db/migrate/20150819135933_add_hidden_at_to_users.rb new file mode 100644 index 000000000..fa54e00e1 --- /dev/null +++ b/db/migrate/20150819135933_add_hidden_at_to_users.rb @@ -0,0 +1,6 @@ +class AddHiddenAtToUsers < ActiveRecord::Migration + def change + add_column :users, :hidden_at, :datetime + add_index :users, :hidden_at + end +end diff --git a/db/schema.rb b/db/schema.rb index f2bbe60fc..6d3005fbf 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: 20150817150457) do +ActiveRecord::Schema.define(version: 20150819135933) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -137,16 +137,18 @@ ActiveRecord::Schema.define(version: 20150817150457) do t.datetime "confirmation_sent_at" t.string "unconfirmed_email" t.string "nickname" - t.string "phone_number", limit: 30 - t.boolean "use_nickname", default: false, null: false - t.boolean "email_on_debate_comment", default: false - t.boolean "email_on_comment_reply", default: false + t.boolean "use_nickname", default: false, null: false + t.boolean "email_on_debate_comment", default: false + t.boolean "email_on_comment_reply", default: false t.string "official_position" - t.integer "official_level", default: 0 + t.integer "official_level", default: 0 + t.string "phone_number", limit: 30 + t.datetime "hidden_at" end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["hidden_at"], name: "index_users_on_hidden_at", using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree create_table "visits", id: :uuid, default: nil, force: :cascade do |t| diff --git a/lib/acts_as_paranoid_aliases.rb b/lib/acts_as_paranoid_aliases.rb index 066ce3e29..dbb0d8350 100644 --- a/lib/acts_as_paranoid_aliases.rb +++ b/lib/acts_as_paranoid_aliases.rb @@ -20,6 +20,16 @@ module ActsAsParanoidAliases def only_hidden only_deleted end + + def hide_all(ids) + return if ids.blank? + where(id: ids).update_all(hidden_at: Time.now) + end + + def restore_all(ids) + return if ids.blank? + only_hidden.where(id: ids).update_all(hidden_at: nil) + end end end diff --git a/spec/features/admin/users_spec.rb b/spec/features/admin/users_spec.rb new file mode 100644 index 000000000..ed5c80f34 --- /dev/null +++ b/spec/features/admin/users_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +feature 'Admin users' do + + scenario 'Restore hidden user' do + citizen = create(:user) + admin = create(:administrator) + create(:moderator, user: admin.user) + + debate_previously_hidden = create(:debate, :hidden, author: citizen) + debate = create(:debate, author: citizen) + comment_previously_hidden = create(:comment, :hidden, user: citizen, commentable: debate, body: "You have the manners of a beggar") + comment = create(:comment, user: citizen, commentable: debate, body: 'Not Spam') + + login_as(admin.user) + visit debate_path(debate) + + within("#debate_#{debate.id}") do + click_link 'Ban author' + end + + visit debates_path + expect(page).to_not have_content(debate.title) + expect(page).to_not have_content(debate_previously_hidden) + + click_link "Administration" + click_link "Hidden users" + click_link "Restore user" + + visit debates_path + expect(page).to have_content(debate.title) + expect(page).to_not have_content(debate_previously_hidden) + + visit debate_path(debate) + expect(page).to have_content(comment.body) + expect(page).to_not have_content(comment_previously_hidden.body) + end + + scenario 'Show user activity' do + citizen = create(:user) + admin = create(:administrator) + create(:moderator, user: admin.user) + + debate1 = create(:debate, :hidden, author: citizen) + debate2 = create(:debate, author: citizen) + comment1 = create(:comment, :hidden, user: citizen, commentable: debate2, body: "You have the manners of a beggar") + comment2 = create(:comment, user: citizen, commentable: debate2, body: 'Not Spam') + + login_as(admin.user) + visit debate_path(debate2) + + within("#debate_#{debate2.id}") do + click_link 'Ban author' + end + + click_link "Administration" + click_link "Hidden users" + click_link citizen.name + + expect(page).to have_content(debate1.title) + expect(page).to have_content(debate2.title) + expect(page).to have_content(comment1.body) + expect(page).to have_content(comment2.body) + end + +end \ No newline at end of file diff --git a/spec/features/moderation/users_spec.rb b/spec/features/moderation/users_spec.rb new file mode 100644 index 000000000..aeea5c088 --- /dev/null +++ b/spec/features/moderation/users_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +feature 'Moderate users' do + + scenario 'Hide' do + citizen = create(:user) + moderator = create(:moderator) + + debate1 = create(:debate, author: citizen) + debate2 = create(:debate, author: citizen) + debate3 = create(:debate) + comment3 = create(:comment, user: citizen, commentable: debate3, body: 'SPAMMER') + + login_as(moderator.user) + visit debates_path + + expect(page).to have_content(debate1.title) + expect(page).to have_content(debate2.title) + expect(page).to have_content(debate3.title) + + visit debate_path(debate3) + + expect(page).to have_content(comment3.body) + + visit debate_path(debate1) + + within("#debate_#{debate1.id}") do + click_link 'Ban author' + end + + expect(current_path).to eq(debates_path) + expect(page).to_not have_content(debate1.title) + expect(page).to_not have_content(debate2.title) + expect(page).to have_content(debate3.title) + + visit debate_path(debate3) + + expect(page).to_not have_content(comment3.body) + + click_link("Logout") + + click_link 'Log in' + fill_in 'user_email', with: citizen.email + fill_in 'user_password', with: citizen.password + click_button 'Log in' + + expect(page).to have_content 'Invalid email or password' + expect(current_path).to eq(new_user_session_path) + end + +end \ No newline at end of file diff --git a/spec/lib/acts_as_paranoid_aliases_spec.rb b/spec/lib/acts_as_paranoid_aliases_spec.rb new file mode 100644 index 000000000..1c99884f3 --- /dev/null +++ b/spec/lib/acts_as_paranoid_aliases_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe 'Paranoid methods' do + + describe '#hide_all' do + it 'hides all instances in the id list' do + debate1 = create(:debate) + debate2 = create(:debate) + debate3 = create(:debate) + debate4 = create(:debate) + + expect(Debate.all.sort).to eq([debate1, debate2, debate3, debate4].sort) + + Debate.hide_all [debate1, debate2, debate4].map(&:id) + + expect(Debate.all).to eq([debate3]) + end + end + + describe '#restore_all' do + it 'restores all instances in the id list' do + debate1 = create(:debate) + debate2 = create(:debate) + debate3 = create(:debate) + + debate1.hide + debate3.hide + + expect(Debate.all).to eq([debate2]) + + Debate.restore_all [debate1, debate3].map(&:id) + + expect(Debate.all.sort).to eq([debate1, debate2, debate3].sort) + end + end + +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 20a3456d4..b484e9e7c 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -64,6 +64,7 @@ describe Ability do describe "Moderator" do let(:user) { create(:user) } before { create(:moderator, user: user) } + let(:other_user) { create(:user) } it { should be_able_to(:index, Debate) } it { should be_able_to(:show, debate) } @@ -88,14 +89,17 @@ describe Ability do it { should be_able_to(:hide, comment) } it { should be_able_to(:hide, debate) } + it { should be_able_to(:hide, other_user) } it { should_not be_able_to(:restore, comment) } it { should_not be_able_to(:restore, debate) } + it { should_not be_able_to(:restore, other_user) } end describe "Administrator" do let(:user) { create(:user) } before { create(:administrator, user: user) } + let(:other_user) { create(:user) } it { should be_able_to(:index, Debate) } it { should be_able_to(:show, debate) } @@ -103,5 +107,6 @@ describe Ability do it { should be_able_to(:restore, comment) } it { should be_able_to(:restore, debate) } + it { should be_able_to(:restore, other_user) } end end