implement story merging

closes #137
This commit is contained in:
joshua stein 2014-04-08 17:51:12 -05:00
parent 9d2253a010
commit 73b8df5eb7
10 changed files with 113 additions and 13 deletions

View file

@ -487,6 +487,15 @@ li .domain {
vertical-align: middle; vertical-align: middle;
} }
.merge {
background: url(data:image/png;base64,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) no-repeat;
width: 11px;
height: 14px;
padding-right: 2px;
display: inline-block;
vertical-align: middle;
}
li .byline { li .byline {
color: #888; color: #888;
font-size: 8.5pt; font-size: 8.5pt;
@ -695,7 +704,8 @@ div#story_box button#story_fetch_title {
padding-bottom: 1px; padding-bottom: 1px;
} }
div#story_box input#story_title, div#story_box input#story_title,
div#story_box input#story_moderation_reason { div#story_box input#story_moderation_reason,
div#story_box input#story_merge_story_short_id {
width: 600px; width: 600px;
} }
div#story_box #story_tags_a { div#story_box #story_tags_a {

View file

@ -174,7 +174,7 @@ private
end end
def _find_stories(how) def _find_stories(how)
stories = Story.where(:is_expired => false) stories = Story.unmerged.where(:is_expired => false)
if @user if @user
hidden_arel = Vote.arel_table.where( hidden_arel = Vote.arel_table.where(

View file

@ -52,6 +52,10 @@ class StoriesController < ApplicationController
end end
@title = "Edit Story" @title = "Edit Story"
if @story.merged_into_story
@story.merge_story_short_id = @story.merged_into_story.short_id
end
end end
def fetch_url_title def fetch_url_title
@ -110,6 +114,11 @@ class StoriesController < ApplicationController
def show def show
@story = Story.where(:short_id => params[:id]).first! @story = Story.where(:short_id => params[:id]).first!
if @story.merged_into_story
flash[:success] = "\"#{@story.title}\" has been merged into this story."
return redirect_to @story.merged_into_story.comments_url
end
if @story.can_be_seen_by_user?(@user) if @story.can_be_seen_by_user?(@user)
@title = @story.title @title = @story.title
else else
@ -118,7 +127,8 @@ class StoriesController < ApplicationController
@short_url = @story.short_id_url @short_url = @story.short_id_url
@comments = @story.comments.includes(:user).arrange_for_user(@user) @comments = @story.merged_comments.includes(:user,
:story).arrange_for_user(@user)
if params[:comment_short_id] if params[:comment_short_id]
@comments.each do |c,x| @comments.each do |c,x|
@ -247,13 +257,13 @@ private
def story_params def story_params
p = params.require(:story).permit( p = params.require(:story).permit(
:title, :url, :description, :moderation_reason, :seen_previous, :title, :url, :description, :moderation_reason, :seen_previous,
:tags_a => [], :merge_story_short_id, :tags_a => [],
) )
if @user.is_moderator? if @user.is_moderator?
p p
else else
p.except(:moderation_reason) p.except(:moderation_reason, :merge_story_short_id)
end end
end end

View file

@ -1,11 +1,19 @@
class Story < ActiveRecord::Base class Story < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :merged_into_story,
:class_name => "Story",
:foreign_key => "merged_story_id"
has_many :merged_stories,
:class_name => "Story",
:foreign_key => "merged_story_id"
has_many :taggings, has_many :taggings,
:autosave => true :autosave => true
has_many :comments, has_many :comments,
:inverse_of => :story :inverse_of => :story
has_many :tags, :through => :taggings has_many :tags, :through => :taggings
scope :unmerged, -> { where(:merged_story_id => nil) }
validates_length_of :title, :in => 3..150 validates_length_of :title, :in => 3..150
validates_length_of :description, :maximum => (64 * 1024) validates_length_of :description, :maximum => (64 * 1024)
validates_presence_of :user_id validates_presence_of :user_id
@ -20,12 +28,13 @@ class Story < ActiveRecord::Base
attr_accessor :vote, :already_posted_story, :fetched_content, :previewing, attr_accessor :vote, :already_posted_story, :fetched_content, :previewing,
:seen_previous :seen_previous
attr_accessor :editor, :moderation_reason attr_accessor :editor, :moderation_reason, :merge_story_short_id
before_validation :assign_short_id_and_upvote, before_validation :assign_short_id_and_upvote,
:on => :create :on => :create
before_save :log_moderation before_save :log_moderation
after_create :mark_submitter, :record_initial_upvote after_create :mark_submitter, :record_initial_upvote
after_save :update_merged_into_story_comments
validate do validate do
if self.url.present? if self.url.present?
@ -284,8 +293,18 @@ class Story < ActiveRecord::Base
elsif all_changes["is_expired"] && !self.is_expired? elsif all_changes["is_expired"] && !self.is_expired?
m.action = "undeleted story" m.action = "undeleted story"
else else
m.action = all_changes.map{|k,v| "changed #{k} from #{v[0].inspect} " << m.action = all_changes.map{|k,v|
"to #{v[1].inspect}" }.join(", ") if k == "merged_story_id"
if v[1]
"merged into #{self.merged_into_story.short_id} " <<
"(#{self.merged_into_story.title})"
else
"unmerged from another story"
end
else
"changed #{k} from #{v[0].inspect} to #{v[1].inspect}"
end
}.join(", ")
end end
m.reason = self.moderation_reason m.reason = self.moderation_reason
@ -302,6 +321,22 @@ class Story < ActiveRecord::Base
Keystore.increment_value_for("user:#{self.user_id}:stories_submitted") Keystore.increment_value_for("user:#{self.user_id}:stories_submitted")
end end
def merge_into_story!(story)
self.merged_story_id = story.id
self.save!
end
def merged_comments
# TODO: make this a normal has_many?
Comment.where(:story_id => Story.select(:id).
where(:merged_story_id => self.id) + [ self.id ])
end
def merge_story_short_id=(sid)
self.merged_story_id = sid.present??
Story.where(:short_id => sid).first.id : nil
end
def recalculate_hotness! def recalculate_hotness!
update_column :hotness, calculated_hotness update_column :hotness, calculated_hotness
end end
@ -375,12 +410,18 @@ class Story < ActiveRecord::Base
end end
def update_comments_count! def update_comments_count!
comments = self.comments.arrange_for_user(nil) comments = self.merged_comments.arrange_for_user(nil)
# calculate count after removing deleted comments and threads # calculate count after removing deleted comments and threads
self.update_column :comments_count, comments.count{|c| !c.is_gone? } self.update_column :comments_count, comments.count{|c| !c.is_gone? }
end end
def update_merged_into_story_comments
if self.merged_into_story
self.merged_into_story.update_comments_count!
end
end
def url=(u) def url=(u)
# strip out stupid google analytics parameters # strip out stupid google analytics parameters
if u && (m = u.match(/\A([^\?]+)\?(.+)\z/)) if u && (m = u.match(/\A([^\?]+)\?(.+)\z/))

View file

@ -23,6 +23,9 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
<% end %> <% end %>
<div class="details"> <div class="details">
<div class="byline"> <div class="byline">
<% if defined?(was_merged) && was_merged %>
<span class="merge"></span>
<% end %>
<% if comment.previewing %> <% if comment.previewing %>
<a><%= comment.user.username %></a> <a><%= comment.user.username %></a>
previewed previewed

View file

@ -36,6 +36,25 @@ class="story <%= story.vote == 1 ? "upvoted" : "" %> <%= story.vote == -1 ?
<% end %> <% end %>
</span> </span>
<span class="domain"><%= story.domain %></span> <span class="domain"><%= story.domain %></span>
<% if defined?(single_story) && single_story %>
<% story.merged_stories.each do |ms| %>
<br>
<span class="merge"></span>
<span class="link">
<a href="<%= ms.url_or_comments_url %>"><%= ms.title %></a>
</span>
<span class="tags">
<% ms.taggings.sort_by{|t| t.tag.tag }.sort_by{|t|
t.tag.tag == "pdf" ? -1 : 0 }.each do |tagging| %>
<a href="<%= tag_url(tagging.tag.tag) %>"
class="<%= tagging.tag.css_class %>"
title="<%= tagging.tag.description %>"><%= tagging.tag.tag %></a>
<% end %>
</span>
<span class="domain"><%= ms.domain %></span>
<% end %>
<% end %>
<% end %> <% end %>
<div class="byline"> <div class="byline">
<% if story.previewing %> <% if story.previewing %>

View file

@ -10,6 +10,13 @@
<% if @user.is_moderator? && (@story.user_id != @user.id) %> <% if @user.is_moderator? && (@story.user_id != @user.id) %>
<div class="box"> <div class="box">
<div class="boxline">
<%= f.label :merge_story_short_id, "Merge Into:",
:class => "required" %>
<%= f.text_field :merge_story_short_id, :autocomplete => "off",
:placeholder => "Short id of story into which this story " <<
"be merged" %>
</div>
<div class="boxline"> <div class="boxline">
<%= f.label :moderation_reason, "Mod Reason:", <%= f.label :moderation_reason, "Mod Reason:",
:class => "required" %> :class => "required" %>

View file

@ -24,7 +24,9 @@
<% while subtree %> <% while subtree %>
<% if (comment = subtree.shift) %> <% if (comment = subtree.shift) %>
<li> <li>
<%= render "comments/comment", :comment => comment %> <%= render "comments/comment", :comment => comment,
:show_story => (comment.story_id != @story.id),
:was_merged => (comment.story_id != @story.id) %>
<% if (children = comments_by_parent[comment.id]) %> <% if (children = comments_by_parent[comment.id]) %>
<% ancestors << subtree %> <% ancestors << subtree %>

View file

@ -0,0 +1,6 @@
class AddStoryMerging < ActiveRecord::Migration
def change
add_column :stories, :merged_story_id, :integer
add_index "stories", [ "merged_story_id" ]
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: 20140221164400) do ActiveRecord::Schema.define(version: 20140408160306) do
create_table "comments", force: true do |t| create_table "comments", force: true do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -103,10 +103,12 @@ ActiveRecord::Schema.define(version: 20140221164400) do
t.text "markeddown_description", limit: 16777215 t.text "markeddown_description", limit: 16777215
t.text "story_cache", limit: 16777215 t.text "story_cache", limit: 16777215
t.integer "comments_count", default: 0, null: false t.integer "comments_count", default: 0, null: false
t.integer "merged_story_id"
end end
add_index "stories", ["hotness"], name: "hotness_idx", using: :btree add_index "stories", ["hotness"], name: "hotness_idx", using: :btree
add_index "stories", ["is_expired", "is_moderated"], name: "is_idxes", using: :btree add_index "stories", ["is_expired", "is_moderated"], name: "is_idxes", using: :btree
add_index "stories", ["merged_story_id"], name: "index_stories_on_merged_story_id", using: :btree
add_index "stories", ["short_id"], name: "unique_short_id", unique: true, using: :btree add_index "stories", ["short_id"], name: "unique_short_id", unique: true, using: :btree
add_index "stories", ["url"], name: "url", length: {"url"=>191}, using: :btree add_index "stories", ["url"], name: "url", length: {"url"=>191}, using: :btree