add moderation logging
- add users.is_moderator and look at that for most things, not is_admin - make default user in readme be a moderator - log moderator actions in story edits, comment deletions/undeletions (and later, user disabling). - remove ability for moderators to edit comments, there's really no reason to.
This commit is contained in:
parent
4692cf63ea
commit
e6c74e8251
|
@ -51,6 +51,7 @@ This is the source code to the site operating at [https://lobste.rs](https://lob
|
|||
Loading development environment (Rails 3.2.6)
|
||||
irb(main):001:0> u = User.new(:username => "test", :email => "test@example.com", :password => "test")
|
||||
irb(main):002:0> u.is_admin = true
|
||||
irb(main):002:0> u.is_moderator = true
|
||||
irb(main):003:0> u.save
|
||||
|
||||
irb(main):004:0> t = Tag.new
|
||||
|
|
|
@ -626,7 +626,8 @@ div#story_preview {
|
|||
div#story_box input#story_url {
|
||||
width: 508px;
|
||||
}
|
||||
div#story_box input#story_title {
|
||||
div#story_box input#story_title,
|
||||
div#story_box input#story_moderation_reason {
|
||||
width: 600px;
|
||||
}
|
||||
div#story_box #story_tags_a {
|
||||
|
|
|
@ -70,7 +70,7 @@ class CommentsController < ApplicationController
|
|||
|
||||
def delete
|
||||
if !((comment = Comment.find_by_short_id(params[:comment_id])) &&
|
||||
comment.is_editable_by_user?(@user))
|
||||
comment.is_deletable_by_user?(@user))
|
||||
return render :text => "can't find comment", :status => 400
|
||||
end
|
||||
|
||||
|
@ -175,9 +175,8 @@ class CommentsController < ApplicationController
|
|||
@title = "Newest Comments"
|
||||
@cur_url = "/comments"
|
||||
|
||||
@comments = Comment.find(:all, :conditions => "is_deleted = 0 AND " +
|
||||
"is_moderated = 0", :order => "created_at DESC", :limit => 20,
|
||||
:include => [ :user, :story ])
|
||||
@comments = Comment.find(:all, :conditions => "is_deleted = 0",
|
||||
:order => "created_at DESC", :limit => 20, :include => [ :user, :story ])
|
||||
|
||||
if @user
|
||||
@votes = Vote.comment_votes_by_user_for_comment_ids_hash(@user.id,
|
||||
|
|
|
@ -74,7 +74,7 @@ private
|
|||
end
|
||||
|
||||
def _find_stories_for_user_and_tag_and_newest(user, tag = nil, newest = false)
|
||||
conds = [ "is_expired = 0 AND is_moderated = 0 " ]
|
||||
conds = [ "is_expired = 0 " ]
|
||||
|
||||
if user && !newest
|
||||
# exclude downvoted items
|
||||
|
|
13
app/controllers/moderations_controller.rb
Normal file
13
app/controllers/moderations_controller.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class ModerationsController < ApplicationController
|
||||
def index
|
||||
@pages = Moderation.count
|
||||
@page = params[:page] ? params[:page].to_i : 0
|
||||
|
||||
if @page < 1
|
||||
@page = 1
|
||||
end
|
||||
|
||||
@moderations = Moderation.order("id desc").limit(50).offset((@page - 1) *
|
||||
50).all
|
||||
end
|
||||
end
|
|
@ -45,10 +45,10 @@ class StoriesController < ApplicationController
|
|||
return redirect_to "/"
|
||||
end
|
||||
|
||||
if @user.is_admin? && @user.id != @story.user_id
|
||||
@story.is_moderated = true
|
||||
else
|
||||
@story.is_expired = true
|
||||
@story.is_expired = true
|
||||
|
||||
if @user.is_moderator? && @user.id != @story.user_id
|
||||
@story.editor_user_id = @user.id
|
||||
end
|
||||
|
||||
@story.save(:validate => false)
|
||||
|
@ -184,7 +184,7 @@ class StoriesController < ApplicationController
|
|||
end
|
||||
|
||||
@story.is_expired = false
|
||||
@story.is_moderated = false
|
||||
@story.editor_user_id = @user.id
|
||||
@story.save(:validate => false)
|
||||
|
||||
redirect_to @story.comments_url
|
||||
|
@ -197,6 +197,7 @@ class StoriesController < ApplicationController
|
|||
end
|
||||
|
||||
@story.is_expired = false
|
||||
@story.editor_user_id = @user.id
|
||||
|
||||
if @story.update_attributes(params[:story].except(:url))
|
||||
return redirect_to @story.comments_url
|
||||
|
@ -244,7 +245,7 @@ class StoriesController < ApplicationController
|
|||
|
||||
private
|
||||
def find_story
|
||||
if @user.is_admin?
|
||||
if @user.is_moderator?
|
||||
@story = Story.find_by_short_id(params[:story_id] || params[:id])
|
||||
else
|
||||
@story = Story.find_by_user_id_and_short_id(@user.id,
|
||||
|
|
|
@ -6,7 +6,7 @@ class Comment < ActiveRecord::Base
|
|||
belongs_to :parent_comment,
|
||||
:class_name => "Comment"
|
||||
|
||||
attr_accessible :comment
|
||||
attr_accessible :comment, :moderation_reason
|
||||
|
||||
attr_accessor :parent_comment_short_id, :current_vote, :previewing,
|
||||
:indent_level, :highlighted
|
||||
|
@ -24,10 +24,10 @@ class Comment < ActiveRecord::Base
|
|||
has "(upvotes - downvotes)", :as => :score, :type => :integer,
|
||||
:sortable => true
|
||||
|
||||
has is_moderated, is_deleted
|
||||
has is_deleted
|
||||
has created_at
|
||||
|
||||
where "is_moderated = 0 AND is_deleted = 0"
|
||||
where "is_deleted = 0"
|
||||
end
|
||||
|
||||
validate do
|
||||
|
@ -81,7 +81,7 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_gone?
|
||||
is_deleted? || is_moderated?
|
||||
is_deleted?
|
||||
end
|
||||
|
||||
def mark_submitter
|
||||
|
@ -112,10 +112,16 @@ class Comment < ActiveRecord::Base
|
|||
def delete_for_user(user)
|
||||
Comment.record_timestamps = false
|
||||
|
||||
if user.is_admin? && user.id != self.user_id
|
||||
self.is_deleted = true
|
||||
|
||||
if user.is_moderator? && user.id != self.user_id
|
||||
self.is_moderated = true
|
||||
else
|
||||
self.is_deleted = true
|
||||
|
||||
m = Moderation.new
|
||||
m.comment_id = self.id
|
||||
m.moderator_user_id = user.id
|
||||
m.action = "deleted comment"
|
||||
m.save
|
||||
end
|
||||
|
||||
self.save(:validate => false)
|
||||
|
@ -127,9 +133,18 @@ class Comment < ActiveRecord::Base
|
|||
def undelete_for_user(user)
|
||||
Comment.record_timestamps = false
|
||||
|
||||
self.is_moderated = false
|
||||
self.is_deleted = false
|
||||
|
||||
if user.is_moderator? && user.id != self.user_id
|
||||
self.is_moderated = true
|
||||
|
||||
m = Moderation.new
|
||||
m.comment_id = self.id
|
||||
m.moderator_user_id = user.id
|
||||
m.action = "undeleted comment"
|
||||
m.save
|
||||
end
|
||||
|
||||
self.save(:validate => false)
|
||||
Comment.record_timestamps = true
|
||||
|
||||
|
@ -233,7 +248,7 @@ class Comment < ActiveRecord::Base
|
|||
if c.is_gone?
|
||||
if ordered[x + 1] && (ordered[x + 1].indent_level > c.indent_level)
|
||||
# we have child comments, so we must stay
|
||||
elsif user && (user.is_admin? || c.user_id == user.id)
|
||||
elsif user && (user.is_moderator? || c.user_id == user.id)
|
||||
# admins and authors should be able to see their deleted comments
|
||||
else
|
||||
# drop this one
|
||||
|
@ -248,9 +263,7 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_editable_by_user?(user)
|
||||
if user && user.is_admin?
|
||||
return true
|
||||
elsif user && user.id == self.user_id
|
||||
if user && user.id == self.user_id
|
||||
if self.is_moderated?
|
||||
return false
|
||||
else
|
||||
|
@ -262,8 +275,18 @@ class Comment < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def is_deletable_by_user?(user)
|
||||
if user && user.is_moderator?
|
||||
return true
|
||||
elsif user && user.id == self.user_id
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def is_undeletable_by_user?(user)
|
||||
if user && user.is_admin?
|
||||
if user && user.is_moderator?
|
||||
return true
|
||||
elsif user && user.id == self.user_id && !self.is_moderated?
|
||||
return true
|
||||
|
|
10
app/models/moderation.rb
Normal file
10
app/models/moderation.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class Moderation < ActiveRecord::Base
|
||||
belongs_to :moderator,
|
||||
:class_name => "User",
|
||||
:foreign_key => "moderator_user_id"
|
||||
belongs_to :story
|
||||
belongs_to :comment
|
||||
belongs_to :user
|
||||
|
||||
attr_accessible nil
|
||||
end
|
|
@ -16,10 +16,12 @@ class Story < ActiveRecord::Base
|
|||
attr_accessor :_comment_count
|
||||
attr_accessor :vote, :already_posted_story, :fetched_content, :previewing
|
||||
attr_accessor :new_tags, :tags_to_add, :tags_to_delete
|
||||
attr_accessor :editor_user_id, :moderation_reason
|
||||
|
||||
attr_accessible :title, :description, :tags_a
|
||||
attr_accessible :title, :description, :tags_a, :moderation_reason
|
||||
|
||||
before_create :assign_short_id
|
||||
before_save :log_moderation
|
||||
after_create :mark_submitter
|
||||
after_save :deal_with_tags
|
||||
|
||||
|
@ -31,7 +33,7 @@ class Story < ActiveRecord::Base
|
|||
indexes tags(:tag), :as => :tags
|
||||
|
||||
has created_at, :sortable => true
|
||||
has hotness, is_moderated, is_expired
|
||||
has hotness, is_expired
|
||||
has "(upvotes - downvotes)", :as => :score, :type => :integer,
|
||||
:sortable => true
|
||||
|
||||
|
@ -40,7 +42,7 @@ class Story < ActiveRecord::Base
|
|||
:tags => 5,
|
||||
}
|
||||
|
||||
where "is_moderated = 0 AND is_expired = 0"
|
||||
where "is_expired = 0"
|
||||
end
|
||||
|
||||
validate do
|
||||
|
@ -118,6 +120,37 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def log_moderation
|
||||
if self.new_record? || self.editor_user_id == self.user_id
|
||||
return
|
||||
end
|
||||
|
||||
m = Moderation.new
|
||||
m.moderator_user_id = self.editor_user_id
|
||||
m.story_id = self.id
|
||||
|
||||
if self.changes["is_expired"] && self.is_expired?
|
||||
m.action = "deleted story"
|
||||
elsif self.changes["is_expired"] && !self.is_expired?
|
||||
m.action = "undeleted story"
|
||||
else
|
||||
actions = self.changes.map{|k,v| "changed #{k} from #{v[0].inspect} " <<
|
||||
"to #{v[1].inspect}" }
|
||||
|
||||
if (old_tags = self.tags.map{|t| t.tag }) != self.tags_a
|
||||
actions.push "changed tags from \"#{old_tags.join(", ")}\" to " <<
|
||||
"\"#{self.tags_a.join(", ")}\""
|
||||
end
|
||||
|
||||
m.action = actions.join(", ")
|
||||
end
|
||||
|
||||
m.reason = self.moderation_reason
|
||||
m.save
|
||||
|
||||
self.is_moderated = true
|
||||
end
|
||||
|
||||
def give_upvote_or_downvote_and_recalculate_hotness!(upvote, downvote)
|
||||
self.upvotes += upvote.to_i
|
||||
self.downvotes += downvote.to_i
|
||||
|
@ -236,7 +269,7 @@ class Story < ActiveRecord::Base
|
|||
def tags_a=(new_tags)
|
||||
self.tags_to_delete = []
|
||||
self.tags_to_add = []
|
||||
self.new_tags = new_tags
|
||||
self.new_tags = new_tags.reject{|t| t.blank? }
|
||||
|
||||
self.tags.each do |tag|
|
||||
if !new_tags.include?(tag.tag)
|
||||
|
@ -252,7 +285,7 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
@_tags_a = new_tags
|
||||
@_tags_a = self.new_tags
|
||||
end
|
||||
|
||||
def url=(u)
|
||||
|
@ -286,7 +319,7 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_editable_by_user?(user)
|
||||
if user && user.is_admin?
|
||||
if user && user.is_moderator?
|
||||
return true
|
||||
elsif user && user.id == self.user_id
|
||||
if self.is_moderated?
|
||||
|
@ -300,7 +333,7 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_undeletable_by_user?(user)
|
||||
if user && user.is_admin?
|
||||
if user && user.is_moderator?
|
||||
return true
|
||||
elsif user && user.id == self.user_id && !self.is_moderated?
|
||||
return true
|
||||
|
@ -310,7 +343,7 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def can_be_seen_by_user?(user)
|
||||
if is_gone? && !(user && (user.is_admin? || user.id == self.user_id))
|
||||
if is_gone? && !(user && (user.is_moderator? || user.id == self.user_id))
|
||||
return false
|
||||
end
|
||||
|
||||
|
@ -318,12 +351,11 @@ class Story < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def is_gone?
|
||||
is_expired? || is_moderated?
|
||||
is_expired?
|
||||
end
|
||||
|
||||
def update_comment_count!
|
||||
Keystore.put("story:#{self.id}:comment_count",
|
||||
Comment.where(:story_id => self.id, :is_moderated => 0,
|
||||
:is_deleted => 0).count)
|
||||
Comment.where(:story_id => self.id, :is_deleted => 0).count)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,7 +48,7 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
<% if comment.is_gone? && comment.is_undeletable_by_user?(@user) %>
|
||||
|
|
||||
<a class="comment_undeletor">undelete</a>
|
||||
<% elsif !comment.is_gone? && comment.is_editable_by_user?(@user) %>
|
||||
<% elsif !comment.is_gone? && comment.is_deletable_by_user?(@user) %>
|
||||
|
|
||||
<a class="comment_deletor">delete</a>
|
||||
<% end %>
|
||||
|
@ -73,8 +73,8 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
|
|||
<% if comment.is_gone? %>
|
||||
<p>
|
||||
<span class="na">
|
||||
[Comment removed by <%= comment.is_deleted? ? "author" :
|
||||
"moderator" %>]
|
||||
[Comment removed by <%= comment.is_moderated? ? "moderator" :
|
||||
"author" %>]
|
||||
</span>
|
||||
</p>
|
||||
<% else %>
|
||||
|
|
|
@ -16,5 +16,5 @@
|
|||
<% end %>
|
||||
<a href="<%= @tag ? "/t/#{@tag.tag}" : (@newest ? "/newest" : "")
|
||||
%>/page/<%= @page + 1 %>">Page <%= @page + 1 %> >></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
54
app/views/moderations/index.html.erb
Normal file
54
app/views/moderations/index.html.erb
Normal file
|
@ -0,0 +1,54 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
Moderation Log
|
||||
</div>
|
||||
|
||||
<table class="data" width="100%" cellspacing=0>
|
||||
<tr>
|
||||
<th width="20%">Date/Time</th>
|
||||
<th width="15%">Moderator</th>
|
||||
<th width="65%">Story/Comment/User, Changes, Reason</th>
|
||||
</tr>
|
||||
<% bit = 0 %>
|
||||
<% @moderations.each do |mod| %>
|
||||
<tr class="row<%= bit %> nobottom">
|
||||
<td><%= mod.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
||||
<td><a href="/messages?to=<%= mod.moderator.try(:username) %>"><%=
|
||||
mod.moderator.try(:username) %></a></td>
|
||||
<td><% if mod.story %>
|
||||
<a href="<%= mod.story.comments_url %>">Story: <%= mod.story.title
|
||||
%></a>
|
||||
<% elsif mod.comment %>
|
||||
<a href="<%= mod.comment.url %>">Comment on <%=
|
||||
mod.comment.story.title %></a>
|
||||
<% elsif mod.user %>
|
||||
User <%= mod.user.try(:username) %>
|
||||
<% end %></td>
|
||||
</tr>
|
||||
<tr class="row<%= bit %> <%= mod.reason.present?? "nobottom" : "" %>">
|
||||
<td colspan=2></td>
|
||||
<td>Action: <em><%= mod.action %></em></td>
|
||||
</tr>
|
||||
<% if mod.reason.present? %>
|
||||
<tr class="row<%= bit %>">
|
||||
<td colspan=2></td>
|
||||
<td>Reason: <em><%= mod.reason %></em></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% bit = (bit == 1 ? 0 : 1) %>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="morelink">
|
||||
<% if @page && @page > 1 %>
|
||||
<a href="/moderations/page/<%= @page - 1 %>"><< Page
|
||||
<%= @page - 1 %></a>
|
||||
<% end %>
|
||||
|
||||
<% if @page && @page > 1 %>
|
||||
|
|
||||
<a href="/moderations/page/<%= @page + 1 %>">Page <%= @page + 1
|
||||
%> >></a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -22,8 +22,8 @@ class="story <%= story.vote == 1 ? "upvoted" : (story.vote == -1 ?
|
|||
<a href="<%= story.url_or_comments_url %>"><%= story.title %></a>
|
||||
<% end %>
|
||||
<% if story.is_gone? %>
|
||||
[Story removed by <%= story.is_expired? ? "original submitter" :
|
||||
"moderator" %>]
|
||||
[Story removed by <%= story.is_moderated? ? "moderator" :
|
||||
"original submitter" %>]
|
||||
<% end %>
|
||||
</span>
|
||||
<% if story.can_be_seen_by_user?(@user) %>
|
||||
|
|
|
@ -8,6 +8,16 @@
|
|||
<%= render :partial => "stories/form", :locals => { :story => @story,
|
||||
:f => f } %>
|
||||
|
||||
<% if @story.user_id != @user.id %>
|
||||
<div class="box">
|
||||
<div class="boxline">
|
||||
<%= f.label :moderation_reason, "Mod Reason:",
|
||||
:class => "required" %>
|
||||
<%= f.text_field :moderation_reason, :autocomplete => "off" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p></p>
|
||||
|
||||
<div class="box">
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
<label class="required">Status:</label>
|
||||
<span class="d">
|
||||
Active <%= @showing_user.is_admin? ? "administrator" : "user" %>
|
||||
Active <%= @showing_user.is_admin? ? "administrator" :
|
||||
(@showing_user.is_moderator? ? "moderator" : "user") %>
|
||||
</span>
|
||||
<br>
|
||||
|
||||
|
|
|
@ -71,4 +71,7 @@ Lobsters::Application.routes.draw do
|
|||
|
||||
post "/invitations" => "invitations#create"
|
||||
get "/invitations/:invitation_code" => "signup#invited"
|
||||
|
||||
get "/moderations" => "moderations#index"
|
||||
get "/moderations/page/:page" => "moderations#index"
|
||||
end
|
||||
|
|
18
db/migrate/20120902143549_add_moderation_log.rb
Normal file
18
db/migrate/20120902143549_add_moderation_log.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
class AddModerationLog < ActiveRecord::Migration
|
||||
def up
|
||||
add_column "users", "is_moderator", :boolean, :default => false
|
||||
|
||||
create_table "moderations" do |t|
|
||||
t.timestamps
|
||||
t.integer "moderator_user_id"
|
||||
t.integer "story_id"
|
||||
t.integer "comment_id"
|
||||
t.integer "user_id"
|
||||
t.text "action"
|
||||
t.text "reason"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20120816203248) do
|
||||
ActiveRecord::Schema.define(:version => 20120902143549) do
|
||||
|
||||
create_table "comments", :force => true do |t|
|
||||
t.datetime "created_at", :null => false
|
||||
|
@ -65,6 +65,17 @@ ActiveRecord::Schema.define(:version => 20120816203248) do
|
|||
|
||||
add_index "messages", ["short_id"], :name => "random_hash", :unique => true
|
||||
|
||||
create_table "moderations", :force => true do |t|
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.integer "moderator_user_id"
|
||||
t.integer "story_id"
|
||||
t.integer "comment_id"
|
||||
t.integer "user_id"
|
||||
t.text "action"
|
||||
t.text "reason"
|
||||
end
|
||||
|
||||
create_table "stories", :force => true do |t|
|
||||
t.datetime "created_at"
|
||||
t.integer "user_id"
|
||||
|
@ -124,6 +135,7 @@ ActiveRecord::Schema.define(:version => 20120816203248) do
|
|||
t.string "pushover_device"
|
||||
t.boolean "email_messages", :default => true
|
||||
t.boolean "pushover_messages", :default => true
|
||||
t.boolean "is_moderator", :default => false
|
||||
end
|
||||
|
||||
add_index "users", ["session_token"], :name => "session_hash", :unique => true
|
||||
|
|
Loading…
Reference in a new issue