diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index c5ce832..9d12b7a 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -211,6 +211,9 @@ $(document).ready(function() { first(); box.html($("#comment_form").clone()); + box.find("ol").remove(); + + box.find("textarea").focus(); var el = $(" [ :create, :preview, :upvote, :downvote, :unvote ] + before_filter :require_logged_in_user, :only => [ :threads ] def create if !(story = Story.find_by_short_id(params[:story_id])) @@ -17,24 +18,33 @@ class CommentsController < ApplicationController params[:parent_comment_short_id]) comment.parent_comment_id = pc.id comment.parent_comment_short_id = pc.short_id + comment.thread_id = pc.thread_id else return render :json => { :error => "invalid parent comment", :status => 400 } end + else + comment.thread_id = Keystore.incremented_value_for("thread_id") end if comment.valid? && !params[:preview].present? && comment.save comment.current_vote = { :vote => 1 } - render :partial => "stories/commentbox", :layout => false, - :content_type => "text/html", :locals => { :story => story, - :comment => Comment.new, :show_comment => comment } + if comment.parent_comment_id + render :partial => "postedreply", :layout => false, + :content_type => "text/html", :locals => { :story => story, + :show_comment => comment } + else + render :partial => "commentbox", :layout => false, + :content_type => "text/html", :locals => { :story => story, + :comment => Comment.new, :show_comment => comment } + end else comment.previewing = true comment.upvotes = 1 comment.current_vote = { :vote => 1 } - render :partial => "stories/commentbox", :layout => false, + render :partial => "commentbox", :layout => false, :content_type => "text/html", :locals => { :story => story, :comment => comment, :show_comment => comment } end @@ -81,4 +91,26 @@ class CommentsController < ApplicationController render :text => "ok" end + + def threads + recent_threads = @user.recent_threads + + @threads = recent_threads.map{|r| + Comment.ordered_for_story_or_thread_for_user(nil, r, @user.id) } + + # trim each thread to this user's first response + @threads.map!{|th| + th.each do |c| + if c.user_id == @user.id + break + else + th.shift + end + end + + th + } + + @comments = @threads.flatten + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index da37d50..9557786 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,7 +2,7 @@ class HomeController < ApplicationController def index conds = [ "is_expired = 0 " ] - if @user + if @user && !@newest # exclude downvoted items conds[0] << "AND stories.id NOT IN (SELECT story_id FROM votes " << "WHERE user_id = ? AND vote < 0) " @@ -35,11 +35,21 @@ class HomeController < ApplicationController end end - @stories.sort_by!{|s| s.hotness } + if @newest + # TODO: better algorithm here + @stories.sort_by!{|s| s.created_at }.reverse! + else + @stories.sort_by!{|s| s.hotness } + end render :action => "index" end + def newest + @newest = true + index + end + def tagged if !(@tag = Tag.find_by_tag(params[:tag])) raise ActionController::RoutingError.new("tag not found") diff --git a/app/controllers/signup_controller.rb b/app/controllers/signup_controller.rb index e726db4..ff615d5 100644 --- a/app/controllers/signup_controller.rb +++ b/app/controllers/signup_controller.rb @@ -1,6 +1,6 @@ class SignupController < ApplicationController def index - @title = "Signup" + @page_title = "Signup" @new_user = User.new end @@ -14,46 +14,4 @@ class SignupController < ApplicationController render :action => "index" end end - -# public function verify() { -# if ($_SESSION["random_hash"] == "") -# return $this->redirect_to("/signup?nocookies=1"); -# -# $this->page_title = "Signup"; -# -# $this->new_user = new User($this->params["user"]); -# $this->new_user->username = $this->new_user->username; -# if ($this->new_user->is_valid()) { -# $error = false; -# try { -# $html = Utils::fetch_url("http://news.ycombinator.com/user?id=" -# . $this->new_user->username); -# } catch (Exception $e) { -# $error = true; -# error_log("error fetching profile for " -# . $this->new_user->username . ": " . $e->getMessage()); -# } -# -# if ($error) { -# $this->add_flash_error("Your Hacker News profile could " -# . "not be fetched at this time. Please try again " -# . "later."); -# return $this->render(array("action" => "index")); -# } elseif (strpos($html, $_SESSION["random_hash"])) { -# $this->new_user->save(); -# -# $this->add_flash_notice("Account created and verified. " -# . "Welcome!"); -# $_SESSION["user_id"] = $this->new_user->id; -# return $this->redirect_to("/"); -# } else { -# $this->add_flash_error("Your Hacker News profile did not " -# . "contain the string provided below. Verify that " -# . "you have cookies enabled and that your Hacker News " -# . "profile has been saved after adding the string."); -# return $this->render(array("action" => "index")); -# } -# } else -# return $this->render(array("action" => "index")); -# } end diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 6fdb1ec..e39ca7c 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -25,6 +25,8 @@ class StoriesController < ApplicationController Vote.vote_thusly_on_story_or_comment_for_user_because(1, @story.already_posted_story.id, nil, @user.id, nil) + flash[:error] = "This URL has already been submitted recently" + return redirect_to @story.already_posted_story.comments_url end @@ -41,6 +43,11 @@ class StoriesController < ApplicationController def edit @page_title = "Edit Story" + + if !@story.is_editable_by_user?(@user) + flash[:error] = "You cannot edit that story" + return redirect_to "/" + end end def fetch_url_title @@ -81,7 +88,8 @@ class StoriesController < ApplicationController @page_title = @story.title - @comments = @story.comments_in_order_for_user(@user ? @user.id : nil) + @comments = Comment.ordered_for_story_or_thread_for_user( + @story.id, nil, @user ? @user.id : nil) @comment = Comment.new if @user diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a6f77df..fa68ebb 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,51 +1,8 @@ class UsersController < ApplicationController -# function settings() { -# if (!$this->user) { -# $this->add_flash_error("You must be logged in to edit your " -# . "settings."); -# return $this->redirect_to("/login"); -# } -# -# $this->page_title = "Edit Settings"; -# -# $this->showing_user = clone $this->user; -# } -# -# function show() { -# if (!($this->showing_user = User::find_by_username($this->params["id"]))) { -# $this->add_flash_error("Could not find user."); -# return $this->redirect_to("/"); -# } -# -# $this->page_title = "User " . $this->showing_user->username; -# -# if (!$this->params["_s"]) -# $this->params["_s"] = NULL; -# -# $this->items = Item::column_sorter($this->params["_s"]); -# $this->items->find("all", -# array("conditions" => array("user_id = ? AND is_expired = 0", -# $this->showing_user->id), -# "include" => array("user", "item_kind"), -# "joins" => array("user"))); -# } -# -# function update() { -# if (!$this->user) { -# $this->add_flash_error("You must be logged in to edit your " -# . "settings."); -# return $this->redirect_to("/login"); -# } -# -# $this->page_title = "Edit Settings"; -# -# $this->showing_user = clone $this->user; -# -# if ($this->showing_user->update_attributes($this->params["user"])) { -# $this->add_flash_notice("Your settings have been updated."); -# return $this->redirect_to(array("controller" => "users", -# "action" => "settings")); -# } else -# return $this->render(array("action" => "settings")); -# } + def show + @showing_user = User.find_by_username!(params[:id]) + + @page_title = "User: #{@showing_user.username}" + + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index a1980e3..4f6a908 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -13,6 +13,8 @@ class Comment < ActiveRecord::Base after_create :assign_votes after_destroy :unassign_votes + MAX_EDIT_MINS = 45 + validate do self.comment.to_s.strip == "" && errors.add(:comment, "cannot be blank.") @@ -85,4 +87,44 @@ class Comment < ActiveRecord::Base def flag! Story.update_counters self.id, :flaggings => 1 end + + def self.ordered_for_story_or_thread_for_user(story_id, thread_id, user_id) + parents = {} + + if thread_id + cs = [ "thread_id = ?", thread_id ] + else + cs = [ "story_id = ?", story_id ] + end + + Comment.find(:all, :conditions => cs).sort_by{|c| c.confidence }.each do |c| + (parents[c.parent_comment_id.to_i] ||= []).push c + end + + # top-down list of comments, regardless of indent level + ordered = [] + + recursor = lambda{|comment,level| + if comment + comment.indent_level = level + ordered.push comment + end + + # for each comment that is a child of this one, recurse with it + (parents[comment ? comment.id : 0] || []).each do |child| + recursor.call(child, level + 1) + end + } + recursor.call(nil, 0) + + ordered + end + + def is_editable_by_user?(user) + if !user || user.id != self.user_id + return false + end + + (Time.now.to_i - self.created_at.to_i < (60 * MAX_EDIT_MINS)) + end end diff --git a/app/models/keystore.rb b/app/models/keystore.rb index a0a2f20..5ab5d43 100644 --- a/app/models/keystore.rb +++ b/app/models/keystore.rb @@ -21,9 +21,9 @@ class Keystore < ActiveRecord::Base def self.incremented_value_for(key, amount = 1) new_value = nil - Keystore.connection.execute([ "INSERT INTO #{Keystore.table_name} (" + - "`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `count` = " + - "`count` + ?", key, amount, amount ]) + Keystore.connection.execute("INSERT INTO #{Keystore.table_name} (" + + "`key`, `value`) VALUES (#{q(key)}, #{q(amount)}) ON DUPLICATE KEY " + + "UPDATE `value` = `value` + #{q(amount)}") return self.value_for(key) end diff --git a/app/models/story.rb b/app/models/story.rb index 51ba83d..2ae6518 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -13,14 +13,29 @@ class Story < ActiveRecord::Base attr_accessible :url, :title, :description, :story_type, :tags_a # after this many minutes old, a story cannot be edited - MAX_EDIT_MINS = 9999 # XXX 15 + MAX_EDIT_MINS = 30 attr_accessor :vote, :story_type, :already_posted_story attr_accessor :tags_to_add, :tags_to_delete after_save :deal_with_tags before_create :assign_short_id - before_create :find_duplicate + + validate do + if self.url.present? + # URI.parse is not very lenient, so we can't use it + + if self.url.match(/\Ahttps?:\/\/[^\.]+\.[a-z]+\//) + if (s = Story.find_by_url(self.url)) && + (Time.now - s.created_at) < 30.days + errors.add(:url, "has already been submitted recently") + self.already_posted_story = s + end + else + errors.add(:url, "is not valid") + end + end + end def assign_short_id (1...10).each do |tries| @@ -35,14 +50,6 @@ class Story < ActiveRecord::Base end end - def find_duplicate - if (s = Story.find_by_url(self.url)) && - (Time.now - s.created_at) < 30.days - errors.add(:url, "has already been submitted recently") - self.already_posted_story = s - end - end - def deal_with_tags (self.tags_to_delete || []).each do |t| if t.is_a?(Tagging) @@ -65,31 +72,6 @@ class Story < ActiveRecord::Base self.tags_to_add = [] end - def comments_in_order_for_user(user_id) - parents = {} - Comment.find_all_by_story_id(self.id).sort_by{|c| c.confidence }.each do |c| - (parents[c.parent_comment_id.to_i] ||= []).push c - end - - # top-down list of comments, regardless of indent level - ordered = [] - - recursor = lambda{|comment,level| - if comment - comment.indent_level = level - ordered.push comment - end - - # for each comment that is a child of this one, recurse with it - (parents[comment ? comment.id : 0] || []).each do |child| - recursor.call(child, level + 1) - end - } - recursor.call(nil, 0) - - ordered - end - def comments_url "/p/#{self.short_id}/#{self.title_as_url}" end @@ -176,7 +158,7 @@ class Story < ActiveRecord::Base return false end - true #(Time.now.to_i - self.created_at.to_i < (60 * Story::MAX_EDIT_MINS)) + (Time.now.to_i - self.created_at.to_i < (60 * MAX_EDIT_MINS)) end def is_undeletable_by_user?(user) diff --git a/app/models/user.rb b/app/models/user.rb index 4c8da27..7b37706 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,10 +31,28 @@ class User < ActiveRecord::Base Keystore.value_for("user:#{self.id}:karma").to_i end + def stories_submitted_count + Keystore.get("user:#{self.id}:stories_submitted").to_i + end + + def comments_posted_count + Keystore.get("user:#{self.id}:comments_posted").to_i + end + def initiate_password_reset_for_ip(ip) self.password_reset_token = Utils.random_key(60) self.save! PasswordReset.password_reset_link(self, ip).deliver end + + def linkified_about + Markdowner.markdown(self.about) + end + + def recent_threads(amount = 20) + Comment.connection.select_all("SELECT DISTINCT " + + "thread_id FROM comments WHERE user_id = #{q(self.id)} ORDER BY " + + "created_at DESC LIMIT #{q(amount)}").map{|r| r.values.first } + end end diff --git a/app/models/vote.rb b/app/models/vote.rb index 249b8ac..ae5522e 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -97,9 +97,17 @@ class Vote < ActiveRecord::Base if v.comment_id Comment.update_counters v.comment_id, :downvotes => downvote, :upvotes => upvote + + if (c = Comment.find(v.comment_id)) && c.user_id != user_id + Keystore.increment_value_for("user:#{c.user_id}:karma") + end else Story.update_counters v.story_id, :downvotes => downvote, :upvotes => upvote + + if (s = Story.find(v.story_id)) && s.user_id != user_id + Keystore.increment_value_for("user:#{s.user_id}:karma") + end end end end diff --git a/app/views/stories/_comment.html.erb b/app/views/comments/_comment.html.erb similarity index 67% rename from app/views/stories/_comment.html.erb rename to app/views/comments/_comment.html.erb index 74083d1..d5bddca 100644 --- a/app/views/stories/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -16,11 +16,13 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
<% if comment.previewing %> <%= comment.user.username %> + previewed just now - (Preview) <% else %> <%= comment.user.username %> + + <%= comment.updated_at ? "edited" : "" %> <%= time_ago_in_words(comment.created_at).gsub(/^about /, "") %> ago <%= comment.current_vote && @@ -28,27 +30,32 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ? "(#{Vote::COMMENT_REASONS[comment.current_vote[:reason]]})" : "" %> <% end %> + + <% if !comment.previewing %> + | + + link + | + <% if comment.is_editable_by_user?(@user) %> + edit + <% else %> + reply + <% end %> + + <% if false && defined?(collapsable) && collapsable # XXX %> + | + collapse + <% end %> + <% end %> + + <% if defined?(show_story) && show_story %> + | on + <%= story.title %> + <% end %>
<%= raw comment.linkified_comment %> - -
- <% if comment.previewing %> - link -   - reply - <% else %> - link -   - reply - - <% if defined?(collapsable) && collapsable %> -   - collapse - <% end %> - <% end %> -
diff --git a/app/views/stories/_commentbox.html.erb b/app/views/comments/_commentbox.html.erb similarity index 95% rename from app/views/stories/_commentbox.html.erb rename to app/views/comments/_commentbox.html.erb index 4f8c9e4..1d0a119 100644 --- a/app/views/stories/_commentbox.html.erb +++ b/app/views/comments/_commentbox.html.erb @@ -33,7 +33,7 @@ <% if defined?(show_comment) %> <% if show_comment.valid? %>
    - <%= render :partial => "stories/comment", + <%= render :partial => "comments/comment", :locals => { :comment => show_comment, :story => story } %>
<% else %> diff --git a/app/views/comments/_postedreply.html.erb b/app/views/comments/_postedreply.html.erb new file mode 100644 index 0000000..5b33c10 --- /dev/null +++ b/app/views/comments/_postedreply.html.erb @@ -0,0 +1,4 @@ +
    + <%= render :partial => "comments/comment", + :locals => { :comment => show_comment, :story => story } %> +
diff --git a/app/views/comments/threads.html.erb b/app/views/comments/threads.html.erb new file mode 100644 index 0000000..fde5c02 --- /dev/null +++ b/app/views/comments/threads.html.erb @@ -0,0 +1,23 @@ +<% cur_story = nil %> +<% indent_level = -1 %> +<% @comments.each_with_index do |comment,x| %> + <% if !cur_story || comment.story_id != cur_story.id %> + <% cur_story = Story.find(comment.story_id) %> + <% end %> + + <% if comment.indent_level > indent_level %> +
    + <% elsif comment.indent_level < indent_level %> + <% (indent_level - comment.indent_level).times do %> +
+ <% end %> + <% end %> + + <%= render :partial => "comments/comment", :locals => { :story => cur_story, + :comment => comment, :show_story => (comment.indent_level == 1) } %> + + <% indent_level = comment.indent_level %> +<% end %> +<% indent_level.times do %> + +<% end %> diff --git a/app/views/global/_header.html.erb b/app/views/global/_header.html.erb index d92bb45..84bf6d1 100644 --- a/app/views/global/_header.html.erb +++ b/app/views/global/_header.html.erb @@ -1,14 +1,17 @@