more work

This commit is contained in:
joshua stein 2012-06-30 14:14:35 -05:00
parent 95b4906e6e
commit 578c96d653
27 changed files with 279 additions and 192 deletions

View file

@ -211,6 +211,9 @@ $(document).ready(function() {
first();
box.html($("#comment_form").clone());
box.find("ol").remove();
box.find("textarea").focus();
var el = $("<input type=\"hidden\" " +
"name=\"parent_comment_short_id\" value=\"" +

View file

@ -291,7 +291,6 @@ ol.comments {
}
ol.comments1 {
margin-left: 0;
margin-top: 2em;
padding-left: 25px;
}
ol.comments.comments1 {

View file

@ -1,6 +1,7 @@
class CommentsController < ApplicationController
before_filter :require_logged_in_user_or_400,
:only => [ :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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,11 +16,13 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
<div class="byline">
<% if comment.previewing %>
<a><%= comment.user.username %></a>
previewed
just now
<span class="reason">(Preview)</span>
<% else %>
<a href="/u/<%= comment.user.username %>"><%= comment.user.username
%></a>
<%= comment.updated_at ? "edited" : "" %>
<%= time_ago_in_words(comment.created_at).gsub(/^about /, "") %> ago
<span class="reason"><%= comment.current_vote &&
@ -28,27 +30,32 @@ class="comment <%= comment.current_vote ? (comment.current_vote[:vote] == 1 ?
"(#{Vote::COMMENT_REASONS[comment.current_vote[:reason]]})" : ""
%></span>
<% end %>
<% if !comment.previewing %>
|
<a href="<%= story.comments_url %>/comments/<%= comment.short_id
%>">link</a>
|
<% if comment.is_editable_by_user?(@user) %>
<a class="comment_editor">edit</a>
<% else %>
<a class="comment_replier">reply</a>
<% end %>
<% if false && defined?(collapsable) && collapsable # XXX %>
|
<a href="">collapse</a>
<% end %>
<% end %>
<% if defined?(show_story) && show_story %>
| on
<a href="<%= story.comments_url %>"><%= story.title %></a>
<% end %>
</div>
<div class="comment_text">
<%= raw comment.linkified_comment %>
<div class="comment_actions">
<% if comment.previewing %>
<a>link</a>
&nbsp;
<a>reply</a>
<% else %>
<a href="<%= story.comments_url %>/comments/<%= comment.short_id
%>">link</a>
&nbsp;
<a class="comment_replier">reply</a>
<% if defined?(collapsable) && collapsable %>
&nbsp;
<a href="">collapse</a>
<% end %>
<% end %>
</div>
</div>
<div class="comment_reply"></div>

View file

@ -33,7 +33,7 @@
<% if defined?(show_comment) %>
<% if show_comment.valid? %>
<ol class="comments comments1 preview">
<%= render :partial => "stories/comment",
<%= render :partial => "comments/comment",
:locals => { :comment => show_comment, :story => story } %>
</ol>
<% else %>

View file

@ -0,0 +1,4 @@
<ol class="comments comments1 preview">
<%= render :partial => "comments/comment",
:locals => { :comment => show_comment, :story => story } %>
</ol>

View file

@ -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 %>
<ol class="comments comments<%= comment.indent_level %>">
<% elsif comment.indent_level < indent_level %>
<% (indent_level - comment.indent_level).times do %>
</ol>
<% 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 %>
</ol>
<% end %>

View file

@ -1,14 +1,17 @@
<div id="header">
<div id="headerright" class="<%= @user ? "loggedin" : "" %>">
<% if @user %>
<a href="/users/<%= @user.username %>"><%= @user.username
<a href="/u/<%= @user.username %>"><%= @user.username
%> (<%= @user.karma %>)</a>
<% if false %>
<% if (count = @user.unread_message_count) > 0 %>
<a href="/messages"><%= count %> New Message<%= count == 1 ? "" : "s"
%></a>
<% else %>
<a href="/messages">Messages</a>
<% end %>
<% end %>
<%= link_to "Logout", { :controller => "login", :action => "logout" },
{ :confirm => "Are you sure you want to logout?",
"method" => "post" } %>
@ -36,7 +39,7 @@
<% if @user %>
<a href="/threads">Your Threads</a>
<% end %>
<a href="/stories/new">Submit</a>
<a href="/stories/new">Submit Story</a>
</span>
<div class="clear"></div>

View file

@ -1,9 +1,3 @@
<% if flash[:error] %>
<div class="flash-error"><%= flash[:error] %></div>
<% elsif flash[:success] %>
<div class="flash-success"><%= flash[:success] %></div>
<% end %>
<ol class="stories list">
<%= render :partial => "stories/listdetail", :collection => @stories,
:as => :story %>

View file

@ -18,6 +18,12 @@
<%= render :partial => "global/header" %>
<div id="inside">
<% if flash[:error] %>
<div class="flash-error"><%= flash[:error] %></div>
<% elsif flash[:success] %>
<div class="flash-success"><%= flash[:success] %></div>
<% end %>
<%= yield %>
</div>

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="box wide">
<div class="legend">
Reset Password
</div>
@ -9,10 +9,6 @@
</p>
<%= form_tag reset_password_url do %>
<% if flash[:error] %>
<div class="flash-error"><%= flash[:error] %></div>
<% end %>
<%= label_tag :email, "E-mail or Username:" %>
<%= text_field_tag :email, "", :size => 30 %>
<br />

View file

@ -4,12 +4,6 @@
</div>
<%= form_tag login_url do %>
<% if flash[:error] %>
<div class="flash-error"><%= flash[:error] %></div>
<% elsif flash[:success] %>
<div class="flash-success"><%= flash[:success] %></div>
<% end %>
<p>
<%= label_tag :email, "E-mail or Username:" %>
<%= text_field_tag :email, "", :size => 30 %>

View file

@ -1,13 +1,9 @@
<div class="box">
<div class="box wide">
<div class="legend">
Set New Password
</div>
<%= form_tag set_new_password_url, { :autocomplete => "off" } do %>
<% if flash[:error] %>
<div class="flash-error"><%= flash[:error] %></div>
<% end %>
<%= error_messages_for(@reset_user) %>
<%= hidden_field_tag "token", params[:token] %>

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="box wide">
<div class="legend">
Create an Account
</div>

View file

@ -12,7 +12,7 @@
<p></p>
<% if @user && !@story.is_expired? %>
<%= render :partial => "stories/commentbox",
<%= render :partial => "comments/commentbox",
:locals => { :story => @story, :comment => @comment } %>
<% end %>
</div>
@ -27,7 +27,7 @@
<% end %>
<% end %>
<%= render :partial => "stories/comment", :locals => { :story => @story,
<%= render :partial => "comments/comment", :locals => { :story => @story,
:comment => comment, :collapsable => (@comments[x + 1].
try(:parent_comment_id).to_i == comment.id) } %>

View file

@ -0,0 +1,42 @@
<div class="box wide">
<div class="legend">
<%= @showing_user.username %>
</div>
<label class="required">Status:</label>
<span class="d">
Active <%= @showing_user.is_admin? ? "administrator" : "user" %>
</span>
<br>
<label class="required">Joined:</label>
<span class="d">
<%= time_ago_in_words(@user.created_at) %> ago
(<%= @user.created_at.strftime("%Y-%m-%d") %>)
</span>
<br>
<label class="required">Karma:</label>
<span class="d">
<%= @showing_user.karma %>
</span>
<br>
<label class="required">Stories Submitted:</label>
<span class="d">
<%= @showing_user.stories_submitted_count %>
</span>
<br>
<label class="required">Comments Posted:</label>
<span class="d">
<%= Keystore.get("user:#{@showing_user.id}:comments_posted").to_i %>
</span>
<br>
<label class="required">About:</label>
<span class="d">
<%= @user.linkified_about %>
</span>
<br>
</div>

View file

@ -1,12 +1,16 @@
Lobsters::Application.routes.draw do
root :to => "home#index"
get "login" => "login#index"
post "login" => "login#login"
post "logout" => "login#logout"
get "/newest" => "home#newest"
get "signup" => "signup#index"
post "signup" => "signup#signup"
get "/threads" => "comments#threads"
get "/login" => "login#index"
post "/login" => "login#login"
post "/logout" => "login#logout"
get "/signup" => "signup#index"
post "/signup" => "signup#signup"
match "/login/forgot_password" => "login#forgot_password",
:as => "forgot_password"
@ -34,4 +38,5 @@ Lobsters::Application.routes.draw do
post "/comments/preview/:story_id" => "comments#preview"
get "/p/:id/(:title)" => "stories#show"
get "/u/:id" => "users#show"
end

View file

@ -8,7 +8,7 @@ class Markdowner
end
def self.markdown(string)
lines = string.rstrip.split(/\r?\n/)
lines = string.to_s.rstrip.split(/\r?\n/)
out = "<p>"
inpre = false