Merge pull request #539 from AyuntamientoMadrid/activities

Activities
This commit is contained in:
Enrique García
2015-09-23 12:40:02 +02:00
25 changed files with 638 additions and 16 deletions

View File

@@ -0,0 +1,8 @@
class Admin::ActivityController < Admin::BaseController
has_filters %w{all on_users on_proposals on_debates on_comments}
def show
@activity = Activity.for_render.send(@current_filter).order(created_at: :desc).page(params[:page])
end
end

View File

@@ -14,6 +14,7 @@ class Admin::CommentsController < Admin::BaseController
def restore
@comment.restore
Activity.log(current_user, :restore, @comment)
redirect_to request.query_parameters.merge(action: :index)
end

View File

@@ -14,6 +14,7 @@ class Admin::DebatesController < Admin::BaseController
def restore
@debate.restore
Activity.log(current_user, :restore, @debate)
redirect_to request.query_parameters.merge(action: :index)
end

View File

@@ -14,6 +14,7 @@ class Admin::ProposalsController < Admin::BaseController
def restore
@proposal.restore
Activity.log(current_user, :restore, @proposal)
redirect_to request.query_parameters.merge(action: :index)
end

View File

@@ -20,6 +20,7 @@ class Admin::UsersController < Admin::BaseController
def restore
@user.restore
Activity.log(current_user, :restore, @user)
redirect_to request.query_parameters.merge(action: :index)
end

View File

@@ -14,21 +14,21 @@ class Moderation::CommentsController < Moderation::BaseController
end
def hide
@comment.hide
hide_comment @comment
end
def moderate
@comments = @comments.where(id: params[:comment_ids])
if params[:hide_comments].present?
@comments.accessible_by(current_ability, :hide).each(&:hide)
@comments.accessible_by(current_ability, :hide).each {|comment| hide_comment comment}
elsif params[:ignore_flags].present?
@comments.accessible_by(current_ability, :ignore_flag).each(&:ignore_flag)
elsif params[:block_authors].present?
author_ids = @comments.pluck(:user_id).uniq
User.where(id: author_ids).accessible_by(current_ability, :block).each(&:block)
User.where(id: author_ids).accessible_by(current_ability, :block).each {|user| block_user user}
end
redirect_to request.query_parameters.merge(action: :index)
@@ -40,4 +40,14 @@ class Moderation::CommentsController < Moderation::BaseController
@comments = Comment.accessible_by(current_ability, :moderate)
end
def hide_comment(comment)
comment.hide
Activity.log(current_user, :hide, comment)
end
def block_user(user)
user.block
Activity.log(current_user, :block, user)
end
end

View File

@@ -14,21 +14,21 @@ class Moderation::DebatesController < Moderation::BaseController
end
def hide
@debate.hide
hide_debate @debate
end
def moderate
@debates = @debates.where(id: params[:debate_ids])
if params[:hide_debates].present?
@debates.accessible_by(current_ability, :hide).each(&:hide)
@debates.accessible_by(current_ability, :hide).each {|debate| hide_debate debate}
elsif params[:ignore_flags].present?
@debates.accessible_by(current_ability, :ignore_flag).each(&:ignore_flag)
elsif params[:block_authors].present?
author_ids = @debates.pluck(:author_id).uniq
User.where(id: author_ids).accessible_by(current_ability, :block).each(&:block)
User.where(id: author_ids).accessible_by(current_ability, :block).each {|user| block_user user}
end
redirect_to request.query_parameters.merge(action: :index)
@@ -40,4 +40,14 @@ class Moderation::DebatesController < Moderation::BaseController
@debates = Debate.accessible_by(current_ability, :moderate)
end
def hide_debate(debate)
debate.hide
Activity.log(current_user, :hide, debate)
end
def block_user(user)
user.block
Activity.log(current_user, :block, user)
end
end

View File

@@ -15,21 +15,21 @@ class Moderation::ProposalsController < Moderation::BaseController
end
def hide
@proposal.hide
hide_proposal @proposal
end
def moderate
@proposals = @proposals.where(id: params[:proposal_ids])
if params[:hide_proposals].present?
@proposals.accessible_by(current_ability, :hide).each(&:hide)
@proposals.accessible_by(current_ability, :hide).each {|proposal| hide_proposal proposal}
elsif params[:ignore_flags].present?
@proposals.accessible_by(current_ability, :ignore_flag).each(&:ignore_flag)
elsif params[:block_authors].present?
author_ids = @proposals.pluck(:author_id).uniq
User.where(id: author_ids).accessible_by(current_ability, :block).each(&:block)
User.where(id: author_ids).accessible_by(current_ability, :block).each {|user| block_user user}
end
redirect_to request.query_parameters.merge(action: :index)
@@ -41,4 +41,14 @@ class Moderation::ProposalsController < Moderation::BaseController
@proposals = Proposal.accessible_by(current_ability, :moderate)
end
def hide_proposal(proposal)
proposal.hide
Activity.log(current_user, :hide, proposal)
end
def block_user(user)
user.block
Activity.log(current_user, :block, user)
end
end

View File

@@ -8,12 +8,14 @@ class Moderation::UsersController < Moderation::BaseController
end
def hide_in_moderation_screen
@user.block
block_user
redirect_to request.query_parameters.merge(action: :index), notice: I18n.t('moderation.users.notice_hide')
end
def hide
@user.block
block_user
redirect_to debates_path
end
@@ -23,4 +25,9 @@ class Moderation::UsersController < Moderation::BaseController
@users = User.with_hidden.search(params[:name_or_email]).page(params[:page]).for_render
end
def block_user
@user.block
Activity.log(current_user, :block, @user)
end
end

28
app/models/activity.rb Normal file
View File

@@ -0,0 +1,28 @@
class Activity < ActiveRecord::Base
belongs_to :actionable, -> { with_hidden }, polymorphic: true
belongs_to :user, -> { with_hidden }
VALID_ACTIONS = %w( hide block restore )
validates :action, inclusion: {in: VALID_ACTIONS}
scope :on_proposals, -> { where(actionable_type: 'Proposal') }
scope :on_debates, -> { where(actionable_type: 'Debate') }
scope :on_users, -> { where(actionable_type: 'User') }
scope :on_comments, -> { where(actionable_type: 'Comment') }
scope :for_render, -> { includes(user: [:moderator, :administrator]).includes(:actionable) }
def self.log(user, action, actionable)
create(user: user, action: action.to_s, actionable: actionable)
end
def self.on(actionable)
where(actionable: actionable)
end
def self.by(user)
where(user: user)
end
end

View File

@@ -73,11 +73,11 @@ class Comment < ActiveRecord::Base
end
def after_hide
commentable_type.constantize.reset_counters(commentable_id, :comments)
commentable_type.constantize.with_hidden.reset_counters(commentable_id, :comments)
end
def after_restore
commentable_type.constantize.reset_counters(commentable_id, :comments)
commentable_type.constantize.with_hidden.reset_counters(commentable_id, :comments)
end
def reply?

View File

@@ -60,6 +60,13 @@
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'activity' %>>
<%= link_to admin_activity_path do %>
<i class="icon-eye"></i>
<%= t('admin.menu.activity') %>
<% end %>
</li>
<li <%= 'class=active' if controller_name == 'settings' %>>
<%= link_to admin_settings_path do %>
<i class="icon-settings"></i>

View File

@@ -0,0 +1,43 @@
<h2><%= t("admin.activity.show.title") %></h2>
<%= render 'shared/filter_subnav', i18n_namespace: "admin.activity.show" %>
<h3><%= page_entries_info @activity %></h3>
<table>
<tr>
<th><%= t("admin.activity.show.type") %></th>
<th><%= t("admin.activity.show.action") %></th>
<th> </th>
<th><%= t("admin.activity.show.by") %></th>
</tr>
<% @activity.each do |activity| %>
<tr id="<%= dom_id(activity) %>">
<td>
<%= activity.actionable_type.constantize.model_name.human %><br>
<span class="date"><%= l activity.actionable.created_at.to_date %></span>
</td>
<td>
<%= t("admin.activity.show.actions.#{activity.action}") %><br>
<span class="date"><%= l activity.created_at.to_date %></span>
</td>
<td>
<% case activity.actionable_type %>
<% when "User" %>
<%= activity.actionable.username %> (<%= activity.actionable.email %>)
<% when "Comment" %>
<%= activity.actionable.body %>
<% else %>
<strong><%= activity.actionable.title %></strong>
<br>
<div class="proposal-description">
<%= activity.actionable.description %>
</div>
<% end %>
</td>
<td><%= activity.user.name %> (<%= activity.user.email %>)</td>
</tr>
<% end %>
</table>
<%= paginate @activity %>

View File

@@ -6,7 +6,7 @@
<ul class="admin-list">
<% @users.each do |user| %>
<li>
<li id="<%= dom_id(user) %>">
<%= link_to user.name, admin_user_path(user) %>
<%= link_to t("admin.actions.restore"),

View File

@@ -108,6 +108,7 @@ ignore_unused:
- 'admin.proposals.index.filter*'
- 'admin.organizations.index.filter*'
- 'admin.users.index.filter*'
- 'admin.activity.show.filter*'
- 'admin.comments.index.hidden_*'
- 'moderation.comments.index.filter*'
- 'moderation.comments.index.order*'

View File

@@ -1,6 +1,7 @@
en:
activerecord:
models:
activity: Activity
comment: Comment
debate: Debate
proposal: Proposal

View File

@@ -1,6 +1,9 @@
es:
activerecord:
models:
activity:
one: actividad
other: actividades
comment:
one: Comentario
other: Comentarios

View File

@@ -20,6 +20,7 @@ en:
officials: Officials
moderators: Moderators
stats: Statistics
activity: Moderation Activity
organizations:
index:
title: Organizations
@@ -129,4 +130,20 @@ en:
moderator:
delete: Delete
add: Add
activity:
show:
title: Activity of Moderators
action: Action
by: Moderated by
type: Type
filter: Show
filters:
all: All
on_proposals: On Proposals
on_debates: On Debates
on_comments: On Comments
on_users: On Users
actions:
hide: Hidden
restore: Restored
block: Blocked

View File

@@ -20,6 +20,7 @@ es:
officials: Cargos públicos
moderators: Moderadores
stats: Estadísticas
activity: Actividad de moderadores
organizations:
index:
title: Organizaciones
@@ -129,3 +130,20 @@ es:
moderator:
delete: Borrar
add: Añadir
activity:
show:
title: Actividad de los Moderadores
action: 'Acción'
by: Moderado por
type: Tipo
filter: Mostrar
filters:
all: Todo
on_proposals: Propuestas
on_debates: Debates
on_comments: Comentarios
on_users: Usuarios
actions:
hide: Ocultado
restore: Restaurado
block: Bloqueado

View File

@@ -112,6 +112,8 @@ Rails.application.routes.draw do
resources :moderators, only: [:index, :create, :destroy] do
collection { get :search }
end
resource :activity, controller: :activity, only: :show
end
namespace :moderation do

View File

@@ -0,0 +1,13 @@
class CreateActivities < ActiveRecord::Migration
def change
create_table :activities do |t|
t.integer :user_id
t.string :action
t.belongs_to :actionable, polymorphic: true
t.timestamps
end
add_index :activities, [:actionable_id, :actionable_type]
add_index :activities, :user_id
end
end

View File

@@ -11,11 +11,23 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150917102718) do
ActiveRecord::Schema.define(version: 20150921095553) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "activities", force: :cascade do |t|
t.integer "user_id"
t.string "action"
t.integer "actionable_id"
t.string "actionable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
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 "addresses", force: :cascade do |t|
t.integer "user_id"
t.string "street"

View File

@@ -33,6 +33,12 @@ FactoryGirl.define do
uid "MyString"
end
factory :activity do
user
action "hide"
association :actionable, factory: :proposal
end
factory :verification_residence, class: Verification::Residence do
user
document_number '12345678Z'

View File

@@ -0,0 +1,334 @@
require 'rails_helper'
feature 'Admin activity' do
background do
@admin = create(:administrator)
login_as(@admin.user)
end
context "Proposals" do
scenario "Shows moderation activity on proposals", :js do
proposal = create(:proposal)
visit proposal_path(proposal)
within("#proposal_#{proposal.id}") do
click_link 'Hide'
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(proposal.title)
expect(page).to have_content("Hidden")
expect(page).to have_content(@admin.user.username)
end
end
scenario "Shows moderation activity from moderation screen" do
proposal1 = create(:proposal)
proposal2 = create(:proposal)
proposal3 = create(:proposal)
visit moderation_proposals_path(filter: 'all')
within("#proposal_#{proposal1.id}") do
check "proposal_#{proposal1.id}_check"
end
within("#proposal_#{proposal3.id}") do
check "proposal_#{proposal3.id}_check"
end
click_on "Hide proposals"
visit admin_activity_path
expect(page).to have_content(proposal1.title)
expect(page).to_not have_content(proposal2.title)
expect(page).to have_content(proposal3.title)
end
scenario "Shows admin restores" do
proposal = create(:proposal, :hidden)
visit admin_proposals_path
within("#proposal_#{proposal.id}") do
click_on "Restore"
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(proposal.title)
expect(page).to have_content("Restored")
expect(page).to have_content(@admin.user.username)
end
end
end
context "Debates" do
scenario "Shows moderation activity on debates", :js do
debate = create(:debate)
visit debate_path(debate)
within("#debate_#{debate.id}") do
click_link 'Hide'
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(debate.title)
expect(page).to have_content("Hidden")
expect(page).to have_content(@admin.user.username)
end
end
scenario "Shows moderation activity from moderation screen" do
debate1 = create(:debate)
debate2 = create(:debate)
debate3 = create(:debate)
visit moderation_debates_path(filter: 'all')
within("#debate_#{debate1.id}") do
check "debate_#{debate1.id}_check"
end
within("#debate_#{debate3.id}") do
check "debate_#{debate3.id}_check"
end
click_on "Hide debates"
visit admin_activity_path
expect(page).to have_content(debate1.title)
expect(page).to_not have_content(debate2.title)
expect(page).to have_content(debate3.title)
end
scenario "Shows admin restores" do
debate = create(:debate, :hidden)
visit admin_debates_path
within("#debate_#{debate.id}") do
click_on "Restore"
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(debate.title)
expect(page).to have_content("Restored")
expect(page).to have_content(@admin.user.username)
end
end
end
context "Comments" do
scenario "Shows moderation activity on comments", :js do
debate = create(:debate)
comment = create(:comment, commentable: debate)
visit debate_path(debate)
within("#comment_#{comment.id}") do
click_link 'Hide'
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(comment.body)
expect(page).to have_content("Hidden")
expect(page).to have_content(@admin.user.username)
end
end
scenario "Shows moderation activity from moderation screen" do
comment1 = create(:comment, body: "SPAM")
comment2 = create(:comment)
comment3 = create(:comment, body: "Offensive!")
visit moderation_comments_path(filter: 'all')
within("#comment_#{comment1.id}") do
check "comment_#{comment1.id}_check"
end
within("#comment_#{comment3.id}") do
check "comment_#{comment3.id}_check"
end
click_on "Hide comments"
visit admin_activity_path
expect(page).to have_content(comment1.body)
expect(page).to_not have_content(comment2.body)
expect(page).to have_content(comment3.body)
end
scenario "Shows admin restores" do
comment = create(:comment, :hidden)
visit admin_comments_path
within("#comment_#{comment.id}") do
click_on "Restore"
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(comment.body)
expect(page).to have_content("Restored")
expect(page).to have_content(@admin.user.username)
end
end
end
context "User" do
scenario "Shows moderation activity on users" do
proposal = create(:proposal)
visit proposal_path(proposal)
within("#proposal_#{proposal.id}") do
click_link 'Ban author'
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content("Blocked")
expect(page).to have_content(proposal.author.username)
expect(page).to have_content(proposal.author.email)
expect(page).to have_content(@admin.user.username)
expect(page).not_to have_content(proposal.title)
end
end
scenario "Shows moderation activity from moderation screen" do
user = create(:user)
visit moderation_users_path(name_or_email: user.username)
within(".admin-list") do
click_link 'Ban'
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(user.username)
expect(page).to have_content(user.email)
expect(page).to have_content(@admin.user.username)
end
end
scenario "Shows moderation activity from proposals moderation screen" do
proposal1 = create(:proposal)
proposal2 = create(:proposal)
proposal3 = create(:proposal)
visit moderation_proposals_path(filter: 'all')
within("#proposal_#{proposal1.id}") do
check "proposal_#{proposal1.id}_check"
end
within("#proposal_#{proposal3.id}") do
check "proposal_#{proposal3.id}_check"
end
click_on "Block authors"
visit admin_activity_path
expect(page).to have_content(proposal1.author.username)
expect(page).to have_content(proposal1.author.email)
expect(page).to have_content(proposal3.author.username)
expect(page).to have_content(proposal3.author.email)
expect(page).to_not have_content(proposal2.author.username)
end
scenario "Shows moderation activity from debates moderation screen" do
debate1 = create(:debate)
debate2 = create(:debate)
debate3 = create(:debate)
visit moderation_debates_path(filter: 'all')
within("#debate_#{debate1.id}") do
check "debate_#{debate1.id}_check"
end
within("#debate_#{debate3.id}") do
check "debate_#{debate3.id}_check"
end
click_on "Block authors"
visit admin_activity_path
expect(page).to have_content(debate1.author.username)
expect(page).to have_content(debate1.author.email)
expect(page).to have_content(debate3.author.username)
expect(page).to have_content(debate3.author.email)
expect(page).to_not have_content(debate2.author.username)
end
scenario "Shows moderation activity from comments moderation screen" do
comment1 = create(:comment, body: "SPAM")
comment2 = create(:comment)
comment3 = create(:comment, body: "Offensive!")
visit moderation_comments_path(filter: 'all')
within("#comment_#{comment1.id}") do
check "comment_#{comment1.id}_check"
end
within("#comment_#{comment3.id}") do
check "comment_#{comment3.id}_check"
end
click_on "Block authors"
visit admin_activity_path
expect(page).to have_content(comment1.author.username)
expect(page).to have_content(comment1.author.email)
expect(page).to have_content(comment3.author.username)
expect(page).to have_content(comment3.author.email)
expect(page).to_not have_content(comment2.author.username)
end
scenario "Shows admin restores" do
user = create(:user, :hidden)
visit admin_users_path
within("#user_#{user.id}") do
click_on "Restore"
end
visit admin_activity_path
within("#activity_#{Activity.last.id}") do
expect(page).to have_content(user.username)
expect(page).to have_content(user.email)
expect(page).to have_content("Restored")
expect(page).to have_content(@admin.user.username)
end
end
end
end

View File

@@ -0,0 +1,88 @@
require 'rails_helper'
describe Activity do
it "should be valid for different actionables" do
expect(build(:activity, actionable: create(:proposal))).to be_valid
expect(build(:activity, actionable: create(:debate))).to be_valid
expect(build(:activity, actionable: create(:comment))).to be_valid
expect(build(:activity, actionable: create(:user))).to be_valid
end
it "should be a valid only with allowed actions" do
expect(build(:activity, action: "hide")).to be_valid
expect(build(:activity, action: "block")).to be_valid
expect(build(:activity, action: "restore")).to be_valid
expect(build(:activity, action: "dissapear")).to_not be_valid
end
describe "log" do
it "should create an activity entry" do
user = create(:user)
proposal = create(:proposal)
expect{ Activity.log(user, :hide, proposal) }.to change { Activity.count }.by(1)
activity = Activity.last
expect(activity.user_id).to eq(user.id)
expect(activity.action).to eq("hide")
expect(activity.actionable).to eq(proposal)
end
end
describe "on" do
it "should list all activity on an actionable object" do
proposal = create(:proposal)
activity1 = create(:activity, action: "hide", actionable: proposal)
activity2 = create(:activity, action: "restore", actionable: proposal)
activity3 = create(:activity, action: "hide", actionable: proposal)
create(:activity, action: "restore", actionable: create(:debate))
create(:activity, action: "hide", actionable: create(:proposal))
create(:activity, action: "hide", actionable: create(:comment))
create(:activity, action: "block", actionable: create(:user))
expect(Activity.on(proposal).size).to eq 3
[activity1, activity2, activity3].each do |a|
expect(Activity.on(proposal)).to include(a)
end
end
end
describe "by" do
it "should list all activity of a user" do
user1 = create(:user)
activity1 = create(:activity, user: user1)
activity2 = create(:activity, user: user1, action: "restore", actionable: create(:debate))
activity3 = create(:activity, user: user1, action: "hide", actionable: create(:proposal))
activity4 = create(:activity, user: user1, action: "hide", actionable: create(:comment))
activity5 = create(:activity, user: user1, action: "block", actionable: create(:user))
create_list(:activity, 3)
expect(Activity.by(user1).size).to eq 5
[activity1, activity2, activity3, activity4, activity5].each do |a|
expect(Activity.by(user1)).to include(a)
end
end
end
describe "scopes by actionable" do
it "should filter by actionable type" do
on_proposal = create(:activity, actionable: create(:proposal))
on_debate = create(:activity, actionable: create(:debate))
on_comment = create(:activity, actionable: create(:comment))
on_user = create(:activity, actionable: create(:user))
expect(Activity.on_proposals.size).to eq 1
expect(Activity.on_debates.size).to eq 1
expect(Activity.on_comments.size).to eq 1
expect(Activity.on_users.size).to eq 1
expect(Activity.on_proposals.first).to eq on_proposal
expect(Activity.on_debates.first).to eq on_debate
expect(Activity.on_comments.first).to eq on_comment
expect(Activity.on_users.first).to eq on_user
end
end
end