Merge pull request #208 from AyuntamientoMadrid/hide-users

Moderate users
This commit is contained in:
Enrique García
2015-08-20 18:00:43 +02:00
24 changed files with 363 additions and 20 deletions

View File

@@ -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

View File

@@ -4,4 +4,5 @@ class Moderation::DebatesController < Moderation::BaseController
@debate = Debate.find(params[:id]) @debate = Debate.find(params[:id])
@debate.hide @debate.hide
end end
end end

View File

@@ -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

View File

@@ -28,11 +28,13 @@ class Ability
can :hide, Comment can :hide, Comment
can :hide, Debate can :hide, Debate
can :hide, User
end end
if user.administrator? if user.administrator?
can :restore, Comment can :restore, Comment
can :restore, Debate can :restore, Debate
can :restore, User
end end
end end
end end

View File

@@ -9,7 +9,7 @@ class Comment < ActiveRecord::Base
validates :user, presence: true validates :user, presence: true
belongs_to :commentable, polymorphic: true belongs_to :commentable, polymorphic: true
belongs_to :user belongs_to :user, -> { with_hidden }
default_scope { includes(:user) } default_scope { includes(:user) }
scope :recent, -> { order(id: :desc) } scope :recent, -> { order(id: :desc) }
@@ -36,6 +36,10 @@ class Comment < ActiveRecord::Base
votes_for.size votes_for.size
end end
def not_visible?
hidden? || user.hidden?
end
# TODO: faking counter cache since there is a bug with acts_as_nested_set :counter_cache # 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 # Remove when https://github.com/collectiveidea/awesome_nested_set/issues/294 is fixed
# and reset counters using # and reset counters using

View File

@@ -11,7 +11,7 @@ class Debate < ActiveRecord::Base
acts_as_taggable acts_as_taggable
acts_as_paranoid column: :hidden_at 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 :title, presence: true
validates :description, presence: true validates :description, presence: true

View File

@@ -1,9 +1,11 @@
class User < ActiveRecord::Base class User < ActiveRecord::Base
include ActsAsParanoidAliases
apply_simple_captcha apply_simple_captcha
devise :database_authenticatable, :registerable, :confirmable, devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
acts_as_voter acts_as_voter
acts_as_paranoid column: :hidden_at
has_one :administrator has_one :administrator
has_one :moderator has_one :moderator

View File

@@ -25,6 +25,13 @@
<% end %> <% end %>
</li> </li>
<li <%= 'class=active' if controller_name == 'users' %>>
<%= link_to admin_users_path do %>
<i class="icon-user"></i>
<%= t('admin.menu.hidden_users') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'organizations' %>> <li <%= 'class=active' if controller_name == 'organizations' %>>
<%= link_to admin_organizations_path do %> <%= link_to admin_organizations_path do %>
<i class="icon-comment-quotes"></i> <i class="icon-comment-quotes"></i>

View File

@@ -0,0 +1,18 @@
<h2><%= t("admin.users.index.title") %></h2>
<h3><%= page_entries_info @users %></h3>
<ul class="admin-list">
<% @users.each do |user| %>
<li>
<%= link_to user.name, admin_user_path(user) %>
<%= link_to t("admin.users.index.restore"), restore_admin_user_path(user),
method: :put, data: { confirm: t('admin.actions.confirm') }, class: "button radius tiny right" %>
</li>
<% end %>
</ul>
<%= paginate @users %>

View File

@@ -0,0 +1,43 @@
<h2><%= t("admin.users.show.title", user: @user.name) %></h2>
<p>
<strong><%= t("admin.users.show.email") %></strong> <%= @user.email %> |
<strong><%= t("admin.users.show.registered_at") %></strong> <%= @user.confirmed_at %> |
<strong><%= t("admin.users.show.hidden_at") %></strong> <%= @user.hidden_at %>
</p>
<p>
<%= 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" %>
</p>
<% if @debates.present? %>
<h3><%= page_entries_info @debates %></h3>
<% end %>
<ul class="admin-list">
<% @debates.each do |debate| %>
<li>
<%= link_to debate.title, admin_debate_path(debate) %>
</li>
<% end %>
</ul>
<% if @comments.present? %>
<h3><%= page_entries_info @comments %></h3>
<% end %>
<ul class="admin-list">
<% @comments.each do |comment| %>
<li id="<%= dom_id(comment) %>">
<div class="row">
<div class="small-12 medium-10 column">
<%= comment.body %>
</div>
</div>
</li>
<% end %>
</ul>
<%= paginate [@debates, @comments].sort_by {|x| x.size}.last %>

View File

@@ -1,5 +1,10 @@
<span id="moderator-comment-actions"> <span id="moderator-comment-actions">
&nbsp;|&nbsp; &nbsp;|&nbsp;
<%= link_to t("admin.actions.hide").capitalize, hide_moderation_comment_path(comment), <%= 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? %>
&nbsp;|&nbsp;
<%= 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 %>
</span> </span>

View File

@@ -1,21 +1,21 @@
<div class="row"> <div class="row">
<div id="<%= dom_id(comment) %>" class="comment small-12 column"> <div id="<%= dom_id(comment) %>" class="comment small-12 column">
<% if comment.hidden? %> <% if comment.not_visible? %>
<%= t("debates.comment.deleted") %> <%= t("debates.comment.deleted") %>
<% else %> <% else %>
<%= avatar_image(comment.user, size: 32, class: 'left') %> <%= avatar_image(comment.user, size: 32, class: 'left') %>
<!-- if comment.user.hidden? <% if comment.user.hidden? %>
<i class="icon-deleted user-deleted"></i> <i class="icon-deleted user-deleted"></i>
end --> <% end %>
<div class="comment-body"> <div class="comment-body">
<div class="comment-info"> <div class="comment-info">
<!-- if comment.user.hidden? <% if comment.user.hidden? %>
<span class="user-name"><%= t("debates.comment.user_deleted") %></span> <span class="user-name"><%= t("debates.comment.user_deleted") %></span>
else --> <% else %>
<span class="user-name"><%= comment.user.name %></span> <span class="user-name"><%= comment.user.name %></span>
<% if comment.user.official? %> <% if comment.user.official? %>
&nbsp;&bullet;&nbsp; &nbsp;&bullet;&nbsp;
@@ -23,7 +23,7 @@
<%= comment.user.official_position %> <%= comment.user.official_position %>
</span> </span>
<% end %> <% end %>
<!-- end --> <% end %>
<% if comment.user.verified_organization? %> <% if comment.user.verified_organization? %>
&nbsp;&bullet;&nbsp; &nbsp;&bullet;&nbsp;
<span class="label round is-association"> <span class="label round is-association">
@@ -70,7 +70,7 @@
<% end %> <% end %>
<div class="comment-children"> <div class="comment-children">
<%= render comment.children.with_deleted.reorder('id DESC, lft') %> <%= render comment.children.with_hidden.reorder('id DESC, lft') %>
</div> </div>
</div> </div>

View File

@@ -1,2 +1,8 @@
<%= link_to t("admin.actions.hide").capitalize, hide_moderation_debate_path(debate), <%= link_to t("admin.actions.hide").capitalize, hide_moderation_debate_path(debate),
method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %> method: :put, remote: true, data: { confirm: t('admin.actions.confirm') } %>
<% unless debate.author.hidden? %>
&nbsp;|&nbsp;
<%= link_to t("admin.actions.hide_author").capitalize, hide_moderation_user_path(debate.author_id),
method: :put, data: { confirm: t('admin.actions.confirm') } %>
<% end %>

View File

@@ -14,12 +14,12 @@
<div class="debate-info"> <div class="debate-info">
<%= avatar_image(@debate.author, size: 32, class: 'author-photo') %> <%= avatar_image(@debate.author, size: 32, class: 'author-photo') %>
<!-- if @debate.author.hidden? %> <% if @debate.author.hidden? %>
<i class="icon-deleted author-deleted"></i> <i class="icon-deleted author-deleted"></i>
<span class="author"> <span class="author">
<%= t("debates.show.author_deleted") %> <%= t("debates.show.author_deleted") %>
</span> </span>
else --> <% else %>
<span class="author"> <span class="author">
<%= @debate.author.name %> <%= @debate.author.name %>
</span> </span>
@@ -29,7 +29,7 @@
<%= @debate.author.official_position %> <%= @debate.author.official_position %>
</span> </span>
<% end %> <% end %>
<!-- end --> <% end %>
<% if @debate.author.verified_organization? %> <% if @debate.author.verified_organization? %>
&nbsp;&bullet;&nbsp; &nbsp;&bullet;&nbsp;
<span class="label round is-association"> <span class="label round is-association">

View File

@@ -13,6 +13,7 @@ en:
debate_topics: Debate topics debate_topics: Debate topics
hidden_debates: Hidden debates hidden_debates: Hidden debates
hidden_comments: Hidden comments hidden_comments: Hidden comments
hidden_users: Hidden users
organizations: Organizations organizations: Organizations
officials: Officials officials: Officials
stats: Statistics stats: Statistics
@@ -31,6 +32,7 @@ en:
rejected: Rejected rejected: Rejected
actions: actions:
hide: Hide hide: Hide
hide_author: Ban author
restore: Restore restore: Restore
confirm: 'Are you sure?' confirm: 'Are you sure?'
tags: tags:
@@ -53,6 +55,19 @@ en:
back: Back back: Back
restore: restore:
success: The debate has been restored 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: officials:
level_0: Level 0 level_0: Level 0
level_1: Level 1 level_1: Level 1

View File

@@ -13,6 +13,7 @@ es:
debate_topics: Temas de debate debate_topics: Temas de debate
hidden_debates: Debates ocultos hidden_debates: Debates ocultos
hidden_comments: Comentarios ocultos hidden_comments: Comentarios ocultos
hidden_users: Usuarios ocultos
organizations: Organizaciones organizations: Organizaciones
officials: Cargos públicos officials: Cargos públicos
stats: Estadísticas stats: Estadísticas
@@ -31,6 +32,7 @@ es:
rejected: Rechazadas rejected: Rechazadas
actions: actions:
hide: Ocultar hide: Ocultar
hide_author: Bloquear al autor
restore: Permitir restore: Permitir
confirm: '¿Estás seguro?' confirm: '¿Estás seguro?'
tags: tags:
@@ -53,6 +55,19 @@ es:
back: Volver back: Volver
restore: restore:
success: El debate ha sido permitido 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: officials:
level_0: Nivel 0 level_0: Nivel 0
level_1: Nivel 1 level_1: Nivel 1

View File

@@ -36,6 +36,10 @@ Rails.application.routes.draw do
end end
end end
resources :users, only: [:index, :show] do
member { put :restore }
end
resources :debates, only: [:index, :show] do resources :debates, only: [:index, :show] do
member { put :restore } member { put :restore }
end end
@@ -55,11 +59,15 @@ Rails.application.routes.draw do
namespace :moderation do namespace :moderation do
root to: "dashboard#index" root to: "dashboard#index"
resources :users, only: [] do
member { put :hide }
end
resources :debates, only: [] do resources :debates, only: [] do
member { put :hide } member { put :hide }
end end
resources :comments, only: [:index] do resources :comments, only: [] do
member { put :hide } member { put :hide }
end end
end end

View File

@@ -0,0 +1,6 @@
class AddHiddenAtToUsers < ActiveRecord::Migration
def change
add_column :users, :hidden_at, :datetime
add_index :users, :hidden_at
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: 20150817150457) do ActiveRecord::Schema.define(version: 20150819135933) 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"
@@ -137,16 +137,18 @@ ActiveRecord::Schema.define(version: 20150817150457) do
t.datetime "confirmation_sent_at" t.datetime "confirmation_sent_at"
t.string "unconfirmed_email" t.string "unconfirmed_email"
t.string "nickname" t.string "nickname"
t.string "phone_number", limit: 30 t.boolean "use_nickname", default: false, null: false
t.boolean "use_nickname", default: false, null: false t.boolean "email_on_debate_comment", default: false
t.boolean "email_on_debate_comment", default: false t.boolean "email_on_comment_reply", default: false
t.boolean "email_on_comment_reply", default: false
t.string "official_position" 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 end
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree 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", ["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 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| create_table "visits", id: :uuid, default: nil, force: :cascade do |t|

View File

@@ -20,6 +20,16 @@ module ActsAsParanoidAliases
def only_hidden def only_hidden
only_deleted only_deleted
end 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
end end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -64,6 +64,7 @@ describe Ability do
describe "Moderator" do describe "Moderator" do
let(:user) { create(:user) } let(:user) { create(:user) }
before { create(:moderator, user: user) } before { create(:moderator, user: user) }
let(:other_user) { create(:user) }
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) }
@@ -88,14 +89,17 @@ describe Ability do
it { should be_able_to(:hide, comment) } it { should be_able_to(:hide, comment) }
it { should be_able_to(:hide, debate) } 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, comment) }
it { should_not be_able_to(:restore, debate) } it { should_not be_able_to(:restore, debate) }
it { should_not be_able_to(:restore, other_user) }
end end
describe "Administrator" do describe "Administrator" do
let(:user) { create(:user) } let(:user) { create(:user) }
before { create(:administrator, user: user) } before { create(:administrator, user: user) }
let(:other_user) { create(:user) }
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) }
@@ -103,5 +107,6 @@ describe Ability do
it { should be_able_to(:restore, comment) } it { should be_able_to(:restore, comment) }
it { should be_able_to(:restore, debate) } it { should be_able_to(:restore, debate) }
it { should be_able_to(:restore, other_user) }
end end
end end