| @@ -0,0 +1,261 @@ | ||
| == Welcome to Rails | ||
| Rails is a web-application framework that includes everything needed to create | ||
| database-backed web applications according to the Model-View-Control pattern. | ||
| This pattern splits the view (also called the presentation) into "dumb" | ||
| templates that are primarily responsible for inserting pre-built data in between | ||
| HTML tags. The model contains the "smart" domain objects (such as Account, | ||
| Product, Person, Post) that holds all the business logic and knows how to | ||
| persist themselves to a database. The controller handles the incoming requests | ||
| (such as Save New Account, Update Product, Show Post) by manipulating the model | ||
| and directing data to the view. | ||
| In Rails, the model is handled by what's called an object-relational mapping | ||
| layer entitled Active Record. This layer allows you to present the data from | ||
| database rows as objects and embellish these data objects with business logic | ||
| methods. You can read more about Active Record in | ||
| link:files/vendor/rails/activerecord/README.html. | ||
| The controller and view are handled by the Action Pack, which handles both | ||
| layers by its two parts: Action View and Action Controller. These two layers | ||
| are bundled in a single package due to their heavy interdependence. This is | ||
| unlike the relationship between the Active Record and Action Pack that is much | ||
| more separate. Each of these packages can be used independently outside of | ||
| Rails. You can read more about Action Pack in | ||
| link:files/vendor/rails/actionpack/README.html. | ||
| == Getting Started | ||
| 1. At the command prompt, create a new Rails application: | ||
| <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name) | ||
| 2. Change directory to <tt>myapp</tt> and start the web server: | ||
| <tt>cd myapp; rails server</tt> (run with --help for options) | ||
| 3. Go to http://localhost:3000/ and you'll see: | ||
| "Welcome aboard: You're riding Ruby on Rails!" | ||
| 4. Follow the guidelines to start developing your application. You can find | ||
| the following resources handy: | ||
| * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html | ||
| * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ | ||
| == Debugging Rails | ||
| Sometimes your application goes wrong. Fortunately there are a lot of tools that | ||
| will help you debug it and get it back on the rails. | ||
| First area to check is the application log files. Have "tail -f" commands | ||
| running on the server.log and development.log. Rails will automatically display | ||
| debugging and runtime information to these files. Debugging info will also be | ||
| shown in the browser on requests from 127.0.0.1. | ||
| You can also log your own messages directly into the log file from your code | ||
| using the Ruby logger class from inside your controllers. Example: | ||
| class WeblogController < ActionController::Base | ||
| def destroy | ||
| @weblog = Weblog.find(params[:id]) | ||
| @weblog.destroy | ||
| logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") | ||
| end | ||
| end | ||
| The result will be a message in your log file along the lines of: | ||
| Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! | ||
| More information on how to use the logger is at http://www.ruby-doc.org/core/ | ||
| Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are | ||
| several books available online as well: | ||
| * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) | ||
| * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) | ||
| These two books will bring you up to speed on the Ruby language and also on | ||
| programming in general. | ||
| == Debugger | ||
| Debugger support is available through the debugger command when you start your | ||
| Mongrel or WEBrick server with --debugger. This means that you can break out of | ||
| execution at any point in the code, investigate and change the model, and then, | ||
| resume execution! You need to install ruby-debug to run the server in debugging | ||
| mode. With gems, use <tt>sudo gem install ruby-debug</tt>. Example: | ||
| class WeblogController < ActionController::Base | ||
| def index | ||
| @posts = Post.all | ||
| debugger | ||
| end | ||
| end | ||
| So the controller will accept the action, run the first line, then present you | ||
| with a IRB prompt in the server window. Here you can do things like: | ||
| >> @posts.inspect | ||
| => "[#<Post:0x14a6be8 | ||
| @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>, | ||
| #<Post:0x14a6620 | ||
| @attributes={"title"=>"Rails", "body"=>"Only ten..", "id"=>"2"}>]" | ||
| >> @posts.first.title = "hello from a debugger" | ||
| => "hello from a debugger" | ||
| ...and even better, you can examine how your runtime objects actually work: | ||
| >> f = @posts.first | ||
| => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}> | ||
| >> f. | ||
| Display all 152 possibilities? (y or n) | ||
| Finally, when you're ready to resume execution, you can enter "cont". | ||
| == Console | ||
| The console is a Ruby shell, which allows you to interact with your | ||
| application's domain model. Here you'll have all parts of the application | ||
| configured, just like it is when the application is running. You can inspect | ||
| domain models, change values, and save to the database. Starting the script | ||
| without arguments will launch it in the development environment. | ||
| To start the console, run <tt>rails console</tt> from the application | ||
| directory. | ||
| Options: | ||
| * Passing the <tt>-s, --sandbox</tt> argument will rollback any modifications | ||
| made to the database. | ||
| * Passing an environment name as an argument will load the corresponding | ||
| environment. Example: <tt>rails console production</tt>. | ||
| To reload your controllers and models after launching the console run | ||
| <tt>reload!</tt> | ||
| More information about irb can be found at: | ||
| link:http://www.rubycentral.org/pickaxe/irb.html | ||
| == dbconsole | ||
| You can go to the command line of your database directly through <tt>rails | ||
| dbconsole</tt>. You would be connected to the database with the credentials | ||
| defined in database.yml. Starting the script without arguments will connect you | ||
| to the development database. Passing an argument will connect you to a different | ||
| database, like <tt>rails dbconsole production</tt>. Currently works for MySQL, | ||
| PostgreSQL and SQLite 3. | ||
| == Description of Contents | ||
| The default directory structure of a generated Ruby on Rails application: | ||
| |-- app | ||
| | |-- assets | ||
| | |-- images | ||
| | |-- javascripts | ||
| | `-- stylesheets | ||
| | |-- controllers | ||
| | |-- helpers | ||
| | |-- mailers | ||
| | |-- models | ||
| | `-- views | ||
| | `-- layouts | ||
| |-- config | ||
| | |-- environments | ||
| | |-- initializers | ||
| | `-- locales | ||
| |-- db | ||
| |-- doc | ||
| |-- lib | ||
| | `-- tasks | ||
| |-- log | ||
| |-- public | ||
| |-- script | ||
| |-- test | ||
| | |-- fixtures | ||
| | |-- functional | ||
| | |-- integration | ||
| | |-- performance | ||
| | `-- unit | ||
| |-- tmp | ||
| | |-- cache | ||
| | |-- pids | ||
| | |-- sessions | ||
| | `-- sockets | ||
| `-- vendor | ||
| |-- assets | ||
| `-- stylesheets | ||
| `-- plugins | ||
| app | ||
| Holds all the code that's specific to this particular application. | ||
| app/assets | ||
| Contains subdirectories for images, stylesheets, and JavaScript files. | ||
| app/controllers | ||
| Holds controllers that should be named like weblogs_controller.rb for | ||
| automated URL mapping. All controllers should descend from | ||
| ApplicationController which itself descends from ActionController::Base. | ||
| app/models | ||
| Holds models that should be named like post.rb. Models descend from | ||
| ActiveRecord::Base by default. | ||
| app/views | ||
| Holds the template files for the view that should be named like | ||
| weblogs/index.html.erb for the WeblogsController#index action. All views use | ||
| eRuby syntax by default. | ||
| app/views/layouts | ||
| Holds the template files for layouts to be used with views. This models the | ||
| common header/footer method of wrapping views. In your views, define a layout | ||
| using the <tt>layout :default</tt> and create a file named default.html.erb. | ||
| Inside default.html.erb, call <% yield %> to render the view using this | ||
| layout. | ||
| app/helpers | ||
| Holds view helpers that should be named like weblogs_helper.rb. These are | ||
| generated for you automatically when using generators for controllers. | ||
| Helpers can be used to wrap functionality for your views into methods. | ||
| config | ||
| Configuration files for the Rails environment, the routing map, the database, | ||
| and other dependencies. | ||
| db | ||
| Contains the database schema in schema.rb. db/migrate contains all the | ||
| sequence of Migrations for your schema. | ||
| doc | ||
| This directory is where your application documentation will be stored when | ||
| generated using <tt>rake doc:app</tt> | ||
| lib | ||
| Application specific libraries. Basically, any kind of custom code that | ||
| doesn't belong under controllers, models, or helpers. This directory is in | ||
| the load path. | ||
| public | ||
| The directory available for the web server. Also contains the dispatchers and the | ||
| default HTML files. This should be set as the DOCUMENT_ROOT of your web | ||
| server. | ||
| script | ||
| Helper scripts for automation and generation. | ||
| test | ||
| Unit and functional tests along with fixtures. When using the rails generate | ||
| command, template test files will be generated for you and placed in this | ||
| directory. | ||
| vendor | ||
| External libraries that the application depends on. Also includes the plugins | ||
| subdirectory. If the app has frozen rails, those gems also go here, under | ||
| vendor/rails/. This directory is in the load path. |
| @@ -0,0 +1,7 @@ | ||
| #!/usr/bin/env rake | ||
| # Add your own tasks in files placed in lib/tasks ending in .rake, | ||
| # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. | ||
| require File.expand_path('../config/application', __FILE__) | ||
| Lobsters::Application.load_tasks |
| @@ -0,0 +1,149 @@ | ||
| //= require jquery | ||
| //= require jquery_ujs | ||
| //= require_tree . | ||
| "use strict"; | ||
| var _Lobsters = Class.extend({ | ||
| commentDownvoteReasons: { "": "Cancel" }, | ||
| storyDownvoteReasons: { "": "Cancel" }, | ||
| upvote: function(story_id) { | ||
| Lobsters.vote("story", story_id, 1); | ||
| }, | ||
| downvote: function(story_id) { | ||
| Lobsters._showDownvoteWhyAt("#story_downvoter_" + story_id, | ||
| function(k) { Lobsters.vote('story', story_id, -1, k); }); | ||
| }, | ||
| upvoteComment: function(comment_id) { | ||
| Lobsters.vote("comment", comment_id, 1); | ||
| }, | ||
| downvoteComment: function(comment_id) { | ||
| Lobsters._showDownvoteWhyAt("#comment_downvoter_" + comment_id, | ||
| function(k) { Lobsters.vote('comment', comment_id, -1, k); }); | ||
| }, | ||
| _showDownvoteWhyAt: function(el, onChooseWhy) { | ||
| if ($("#downvote_why")) | ||
| $("#downvote_why").remove(); | ||
| var d = $("<div id=\"downvote_why\"></div>"); | ||
| var reasons; | ||
| if ($(el).attr("id").match(/comment/)) | ||
| reasons = Lobsters.commentDownvoteReasons; | ||
| else | ||
| reasons = Lobsters.storyDownvoteReasons; | ||
| $.each(reasons, function(k, v) { | ||
| var a = $("<a href=\"#\">" + v + "</a>"); | ||
| a.click(function() { | ||
| $('#downvote_why').remove(); | ||
| if (k != "") | ||
| onChooseWhy(k); | ||
| return false; | ||
| }); | ||
| d.append(a); | ||
| }); | ||
| $(el).after(d); | ||
| d.position({ | ||
| my: "left top", | ||
| at: "left bottom", | ||
| offset: "-2 -2", | ||
| of: $(el), | ||
| collision: "none", | ||
| }); | ||
| }, | ||
| vote: function(thing_type, thing_id, point, reason) { | ||
| var li = $("#" + thing_type + "_" + thing_id); | ||
| var score_d = li.find("div.score").get(0); | ||
| var score = parseInt(score_d.innerHTML); | ||
| var action = ""; | ||
| if (li.hasClass("upvoted") && point > 0) { | ||
| /* already upvoted, neutralize */ | ||
| li.removeClass("upvoted"); | ||
| score--; | ||
| action = "unvote"; | ||
| } | ||
| else if (li.hasClass("downvoted") && point < 0) { | ||
| /* already downvoted, neutralize */ | ||
| li.removeClass("downvoted"); | ||
| score++; | ||
| action = "unvote"; | ||
| } | ||
| else if (point > 0) { | ||
| if (li.hasClass("downvoted")) | ||
| /* flip flop */ | ||
| score++; | ||
| li.removeClass("downvoted").addClass("upvoted"); | ||
| score++; | ||
| action = "upvote"; | ||
| } | ||
| else if (point < 0) { | ||
| if (li.hasClass("upvoted")) | ||
| /* flip flop */ | ||
| score--; | ||
| li.removeClass("upvoted").addClass("downvoted"); | ||
| score--; | ||
| action = "downvote"; | ||
| } | ||
| score_d.innerHTML = score; | ||
| $.post("/" + (thing_type == "story" ? "stories" : | ||
| thing_type + "s") + "/" + thing_id + "/" + action, | ||
| { why: reason }); | ||
| }, | ||
| postComment: function(form) { | ||
| $(form).load($(form).attr("action"), $(form).serializeArray()); | ||
| }, | ||
| previewComment: function(form) { | ||
| $(form).load($(form).attr("action").replace(/^\/comments/, | ||
| "/comments/preview"), $(form).serializeArray()); | ||
| }, | ||
| fetchURLTitle: function(button, url_field, title_field) { | ||
| if (url_field.val() == "") | ||
| return; | ||
| var old_value = button.val(); | ||
| button.prop("disabled", true); | ||
| button.val("Fetching..."); | ||
| $.post("/stories/fetch_url_title", { | ||
| fetch_url: url_field.val(), | ||
| }) | ||
| .success(function(data) { | ||
| if (data && data.title) | ||
| title_field.val(data.title.substr(0, | ||
| title_field.maxLength)); | ||
| button.val(old_value); | ||
| button.prop("disabled", false); | ||
| }) | ||
| .error(function() { | ||
| button.val(old_value); | ||
| button.prop("disabled", false); | ||
| }); | ||
| }, | ||
| }); | ||
| var Lobsters = new _Lobsters(); | ||
| /* FIXME */ | ||
| /* $(document).click(function() { | ||
| $("#downvote_why").remove(); | ||
| }); */ |
| @@ -0,0 +1,64 @@ | ||
| /* Simple JavaScript Inheritance | ||
| * By John Resig http://ejohn.org/ | ||
| * MIT Licensed. | ||
| */ | ||
| // Inspired by base2 and Prototype | ||
| (function(){ | ||
| var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; | ||
| // The base Class implementation (does nothing) | ||
| this.Class = function(){}; | ||
| // Create a new Class that inherits from this class | ||
| Class.extend = function(prop) { | ||
| var _super = this.prototype; | ||
| // Instantiate a base class (but only create the instance, | ||
| // don't run the init constructor) | ||
| initializing = true; | ||
| var prototype = new this(); | ||
| initializing = false; | ||
| // Copy the properties over onto the new prototype | ||
| for (var name in prop) { | ||
| // Check if we're overwriting an existing function | ||
| prototype[name] = typeof prop[name] == "function" && | ||
| typeof _super[name] == "function" && fnTest.test(prop[name]) ? | ||
| (function(name, fn){ | ||
| return function() { | ||
| var tmp = this._super; | ||
| // Add a new ._super() method that is the same method | ||
| // but on the super-class | ||
| this._super = _super[name]; | ||
| // The method only need to be bound temporarily, so we | ||
| // remove it when we're done executing | ||
| var ret = fn.apply(this, arguments); | ||
| this._super = tmp; | ||
| return ret; | ||
| }; | ||
| })(name, prop[name]) : | ||
| prop[name]; | ||
| } | ||
| // The dummy class constructor | ||
| function Class() { | ||
| // All construction is actually done in the init method | ||
| if ( !initializing && this.init ) | ||
| this.init.apply(this, arguments); | ||
| } | ||
| // Populate our constructed prototype object | ||
| Class.prototype = prototype; | ||
| // Enforce the constructor to be what we expect | ||
| Class.prototype.constructor = Class; | ||
| // And make this class extendable | ||
| Class.extend = arguments.callee; | ||
| return Class; | ||
| }; | ||
| })(); |
| @@ -0,0 +1,29 @@ | ||
| class ApplicationController < ActionController::Base | ||
| protect_from_forgery | ||
| before_filter :authenticate_user | ||
| def authenticate_user | ||
| if session[:u] | ||
| @user = User.find_by_session_token(session[:u]) | ||
| end | ||
| true | ||
| end | ||
| def require_logged_in_user | ||
| if @user | ||
| true | ||
| else | ||
| redirect_to "/login" | ||
| end | ||
| end | ||
| def require_logged_in_user_or_400 | ||
| if @user | ||
| true | ||
| else | ||
| render :text => "not logged in", :status => 400 | ||
| return false | ||
| end | ||
| end | ||
| end |
| @@ -0,0 +1,75 @@ | ||
| class CommentsController < ApplicationController | ||
| before_filter :require_logged_in_user_or_400, | ||
| :only => [ :create, :preview, :upvote, :downvote, :unvote ] | ||
| def create | ||
| if !(story = Story.find_by_short_id(params[:story_id])) | ||
| return render :text => "can't find story", :status => 400 | ||
| end | ||
| comment = Comment.new | ||
| comment.comment = params[:comment].to_s | ||
| comment.story_id = story.id | ||
| comment.user_id = @user.id | ||
| comment.upvotes = 1 | ||
| if params[:parent_comment_short_id] | ||
| if pc = Comment.find_by_story_id_and_short_id(story.id, | ||
| params[:parent_comment_short_id]) | ||
| comment.parent_comment_id = pc.id | ||
| else | ||
| return render :json => { :error => "invalid parent comment", | ||
| :status => 400 } | ||
| end | ||
| end | ||
| if comment.valid? && params[:preview].blank? | ||
| comment.save | ||
| end | ||
| render :partial => "stories/commentbox", :layout => false, | ||
| :locals => { :story => story, :comment => comment } | ||
| end | ||
| def preview | ||
| params[:preview] = true | ||
| return create | ||
| end | ||
| def unvote | ||
| if !(comment = Comment.find_by_short_id(params[:comment_id])) | ||
| return render :text => "can't find comment", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(0, comment.story_id, | ||
| comment.id, @user.id, nil) | ||
| render :text => "ok" | ||
| end | ||
| def upvote | ||
| if !(comment = Comment.find_by_short_id(params[:comment_id])) | ||
| return render :text => "can't find comment", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(1, comment.story_id, | ||
| comment.id, @user.id, params[:reason]) | ||
| render :text => "ok" | ||
| end | ||
| def downvote | ||
| if !(comment = Comment.find_by_short_id(params[:comment_id])) | ||
| return render :text => "can't find comment", :status => 400 | ||
| end | ||
| if !Vote::COMMENT_REASONS[params[:reason]] | ||
| return render :text => "invalid reason", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(-1, comment.story_id, | ||
| comment.id, @user.id, params[:reason]) | ||
| render :text => "ok" | ||
| end | ||
| end |
| @@ -0,0 +1,50 @@ | ||
| class HomeController < ApplicationController | ||
| def index | ||
| conds = [ "is_expired = 0 " ] | ||
| if @user | ||
| # exclude downvoted items | ||
| conds[0] << "AND stories.id NOT IN (SELECT story_id FROM votes " << | ||
| "WHERE user_id = ? AND vote < 0) " | ||
| conds.push @user.id | ||
| end | ||
| if @tag | ||
| conds[0] << "AND taggings.tag_id = ?" | ||
| conds.push @tag.id | ||
| @stories = Story.find(:all, :conditions => conds, | ||
| :include => [ :user, :taggings ], :joins => [ :user, :taggings ], | ||
| :limit => 30) | ||
| @title = @tag.description.blank?? @tag.tag : @tag.description | ||
| @title_url = tag_url(@tag.tag) | ||
| else | ||
| @stories = Story.find(:all, :conditions => conds, | ||
| :include => [ :user, :taggings ], :joins => [ :user ], | ||
| :limit => 30) | ||
| end | ||
| if @user | ||
| votes = Vote.votes_by_user_for_stories_hash(@user.id, | ||
| @stories.map{|s| s.id }) | ||
| @stories.each do |s| | ||
| if votes[s.id] | ||
| s.vote = votes[s.id] | ||
| end | ||
| end | ||
| end | ||
| @stories.sort_by!{|s| s.hotness } | ||
| render :action => "index" | ||
| end | ||
| def tagged | ||
| if !(@tag = Tag.find_by_tag(params[:tag])) | ||
| raise ActionController::RoutingError.new("tag not found") | ||
| end | ||
| index | ||
| end | ||
| end |
| @@ -0,0 +1,68 @@ | ||
| class LoginController < ApplicationController | ||
| before_filter :authenticate_user | ||
| def logout | ||
| if @user | ||
| reset_session | ||
| end | ||
| redirect_to "/" | ||
| end | ||
| def index | ||
| @page_title = "Login" | ||
| render :action => "index" | ||
| end | ||
| def login | ||
| if (user = User.where("email = ? OR username = ?", params[:email], | ||
| params[:email]).first) && user.try(:authenticate, params[:password]) | ||
| session[:u] = user.session_token | ||
| return redirect_to "/" | ||
| end | ||
| flash[:error] = "Invalid e-mail address and/or password." | ||
| index | ||
| end | ||
| def forgot_password | ||
| @page_title = "Reset Password" | ||
| render :action => "forgot_password" | ||
| end | ||
| def reset_password | ||
| @found_user = User.where("email = ? OR username = ?", params[:email], | ||
| params[:email]).first | ||
| if !@found_user | ||
| flash[:error] = "Invalid e-mail address or username." | ||
| return forgot_password | ||
| end | ||
| @found_user.initiate_password_reset_for_ip(request.remote_ip) | ||
| flash[:success] = "Password reset instructions have been e-mailed to you." | ||
| return index | ||
| end | ||
| def set_new_password | ||
| if params[:token].blank? || | ||
| !(@reset_user = User.find_by_password_reset_token(params[:token])) | ||
| flash[:error] = "Invalid reset token. It may have already been " << | ||
| "used or you may have copied it incorrectly." | ||
| return redirect_to forgot_password_url | ||
| end | ||
| if !params[:password].blank? | ||
| @reset_user.password = params[:password] | ||
| @reset_user.password_confirmation = params[:password_confirmation] | ||
| @reset_user.session_token = nil | ||
| @reset_user.password_reset_token = nil | ||
| if @reset_user.save | ||
| session[:u] = @reset_user.session_token | ||
| return redirect_to "/" | ||
| end | ||
| end | ||
| end | ||
| end |
| @@ -0,0 +1,107 @@ | ||
| class MessagesController < ApplicationController | ||
| # static $verify = array( | ||
| # array("method" => "post", | ||
| # "only" => array("reply", "send"), | ||
| # "redirect_to" => "/", | ||
| # ), | ||
| # ); | ||
| # | ||
| # public function index() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to read messages."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # $this->page_title = "Your Messages"; | ||
| # | ||
| # $this->incoming_messages = | ||
| # Message::find_all_by_recipient_user_id($this->user->id, | ||
| # array("order" => "created_at DESC")); | ||
| # | ||
| # $this->sent_messages = | ||
| # Message::find_all_by_author_user_id($this->user->id, | ||
| # array("order" => "created_at DESC")); | ||
| # } | ||
| # | ||
| # public function show() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to read messages."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # if (!($this->message = Message::find_by_random_hash($this->params["id"]))) { | ||
| # $this->add_flash_error("Could not find message."); | ||
| # return $this->redirect_to(array("controller" => "messages")); | ||
| # } | ||
| # | ||
| # if (!($this->message->recipient_user_id == $this->user->id || | ||
| # $this->message->author_user_id == $this->user->id)) { | ||
| # $this->add_flash_error("Could not find message."); | ||
| # return $this->redirect_to(array("controller" => "messages")); | ||
| # } | ||
| # | ||
| # if ($this->message->recipient_user_id == $this->user->id && | ||
| # !$this->message->has_been_read) { | ||
| # $this->message->has_been_read = true; | ||
| # $this->message->save(); | ||
| # } | ||
| # | ||
| # $this->page_title = "Message From " | ||
| # . $this->message->author->username . " To " | ||
| # . $this->message->recipient->username; | ||
| # | ||
| # $this->reply = new Message; | ||
| # $this->reply->author_user_id = $this->user->id; | ||
| # $this->reply->recipient_user_id = $this->message->author_user_id; | ||
| # $this->reply->subject = preg_match("/^re[: ]/i", | ||
| # $this->message->subject) ? "" : "Re: " . $this->message->subject; | ||
| # } | ||
| # | ||
| # /* id is a message id */ | ||
| # public function reply() { | ||
| # $this->show(); | ||
| # | ||
| # $this->page_title = "Message From " | ||
| # . $this->message->author->username . " To " | ||
| # . $this->message->recipient->username; | ||
| # | ||
| # if ($this->reply->update_attributes($this->params["message"])) { | ||
| # $this->add_flash_notice("Your reply has been sent."); | ||
| # return $this->redirect_to(array("controller" => "messages")); | ||
| # } else { | ||
| # return $this->render(array("action" => "show")); | ||
| # } | ||
| # } | ||
| # | ||
| # /* id is a username */ | ||
| # public function compose() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to send messages."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # if (!($this->recipient_user = | ||
| # User::find_by_username($this->params["id"]))) { | ||
| # $this->add_flash_error("Could not find recipient user."); | ||
| # return $this->redirect_to("/messages"); | ||
| # } | ||
| # | ||
| # $this->page_title = "Compose Message To " | ||
| # . $this->recipient_user->username; | ||
| # | ||
| # $this->message = new Message; | ||
| # $this->message->recipient_user_id = $this->recipient_user->id; | ||
| # $this->message->author_user_id = $this->user->id; | ||
| # } | ||
| # | ||
| # public function send() { | ||
| # $this->compose(); | ||
| # | ||
| # if ($this->message->update_attributes($this->params["message"])) { | ||
| # $this->add_flash_notice("Your message has been sent."); | ||
| # return $this->redirect_to(array("controller" => "messages")); | ||
| # } else { | ||
| # return $this->render(array("action" => "compose")); | ||
| # } | ||
| # } | ||
| end |
| @@ -0,0 +1,59 @@ | ||
| class SignupController < ApplicationController | ||
| def index | ||
| @title = "Signup" | ||
| @new_user = User.new | ||
| end | ||
| def signup | ||
| @new_user = User.new(params[:user]) | ||
| if @new_user.save | ||
| session[:u] = @new_user.session_hash | ||
| return redirect_to "/" | ||
| else | ||
| 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 |
| @@ -0,0 +1,225 @@ | ||
| class StoriesController < ApplicationController | ||
| before_filter :require_logged_in_user_or_400, | ||
| :only => [ :upvote, :downvote, :unvote ] | ||
| before_filter :require_logged_in_user, :only => [ :delete, :create, :edit, | ||
| :fetch_url_title, :new ] | ||
| def new | ||
| @page_title = "Submit a New Story" | ||
| @story = Story.new | ||
| @story.story_type = "link" | ||
| if !params[:url].blank? | ||
| @story.url = params[:url] | ||
| if !params[:title].blank? | ||
| @story.title = params[:title] | ||
| end | ||
| end | ||
| end | ||
| def create | ||
| @page_title = "Submit a New Story" | ||
| @story = Story.new(params[:story]) | ||
| @story.user_id = @user.id | ||
| if @story.save | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(1, @story.id, | ||
| nil, @user.id, nil) | ||
| return redirect_to @story.comments_url | ||
| else | ||
| if @story.already_posted_story? | ||
| # consider it an upvote | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(1, | ||
| @story.already_posted_story.id, nil, @user.id, nil) | ||
| return redirect_to @story.already_posted_story.comments_url | ||
| end | ||
| return render :action => "new" | ||
| end | ||
| end | ||
| def delete | ||
| if @user.is_admin? | ||
| @story = Story.find_by_short_id(params[:id]) | ||
| else | ||
| @story = Story.find_by_user_id_and_short_id(@user.id, params[:id]) | ||
| end | ||
| if !@story | ||
| flash[:error] = "Could not find story or you are not authorized to " << | ||
| "delete it." | ||
| return redirect_to "/" | ||
| end | ||
| @story.is_expired = true | ||
| @story.save | ||
| redirect_to @story.comments_url | ||
| end | ||
| # public function edit() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to edit a story."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # $this->story = Story::find_by_user_id_and_short_id($this->user->id, | ||
| # $this->params["id"]); | ||
| # | ||
| # if (!$this->story) { | ||
| # $this->add_flash_error("Could not find story or you are not " | ||
| # . "authorized to edit it."); | ||
| # return $this->redirect_to("/"); | ||
| # } | ||
| # | ||
| # $this->page_title = "Editing " . $this->story->title; | ||
| # } | ||
| # | ||
| def fetch_url_title | ||
| begin | ||
| s = Sponge.new | ||
| s.timeout = 3 | ||
| text = s.fetch(params[:fetch_url], :get, nil, nil, | ||
| { "User-agent" => "lobste.rs! via #{request.remote_ip}" }, 3) | ||
| if m = text.match(/<\s*title\s*>([^<]+)<\/\s*title\s*>/i) | ||
| return render :json => { :title => m[1] } | ||
| else | ||
| raise "no title found" | ||
| end | ||
| rescue => e | ||
| return render :json => "error" | ||
| end | ||
| end | ||
| # public function index() { | ||
| # $this->items = Item::find("all"); | ||
| # } | ||
| # | ||
| # public function manage() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to manage your " | ||
| # . "items."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # $this->page_title = "Manage Your Items"; | ||
| # | ||
| # $this->items = Item::column_sorter($this->params["_s"]); | ||
| # $this->items->find("all", | ||
| # array("conditions" => array("user_id = ?", $this->user->id), | ||
| # "include" => array("user", "item_kind"), | ||
| # "joins" => array("user"))); | ||
| # } | ||
| # | ||
| # public function message() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to edit an item."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # $this->show(); | ||
| # | ||
| # if ($this->new_message->update_attributes($this->params["message"])) { | ||
| # $this->add_flash_notice("Your message has been sent."); | ||
| # return $this->redirect_to(array("controller" => "items", | ||
| # "action" => "show", "id" => $this->item->id)); | ||
| # } else { | ||
| # return $this->render(array("action" => "items/show")); | ||
| # } | ||
| # } | ||
| # | ||
| def show | ||
| @story = Story.find_by_short_id!(params[:id]) | ||
| @page_title = @story.title | ||
| @comments = @story.comments_in_order_for_user(@user ? @user.id : nil) | ||
| @comment = Comment.new | ||
| if @user | ||
| if v = Vote.find_by_user_id_and_story_id(@user.id, @story.id) | ||
| @story.vote = v.vote | ||
| end | ||
| @votes = Vote.comment_votes_by_user_for_story_hash(@user.id, @story.id) | ||
| @comments.each do |c| | ||
| if @votes[c.id] | ||
| c.vote = @votes[c.id] | ||
| end | ||
| end | ||
| end | ||
| end | ||
| # public function update() { | ||
| # if (!$this->user) { | ||
| # $this->add_flash_error("You must be logged in to edit an item."); | ||
| # return $this->redirect_to("/login"); | ||
| # } | ||
| # | ||
| # if ($this->user->is_admin) | ||
| # $this->item = Item::find_by_id($this->params["id"]); | ||
| # else | ||
| # $this->item = Item::find_by_user_id_and_id($this->user->id, | ||
| # $this->params["id"]); | ||
| # | ||
| # if (!$this->item) { | ||
| # $this->add_flash_error("Could not find item or you are not " | ||
| # . "authorized to edit it."); | ||
| # return $this->redirect_to("/"); | ||
| # } | ||
| # | ||
| # $this->item->is_expired = false; | ||
| # if ($this->item->update_attributes($this->params["item"])) { | ||
| # $this->add_flash_notice("Successfully saved item changes."); | ||
| # return $this->redirect_to(array("controller" => "items", | ||
| # "action" => "show", "id" => $this->item->id)); | ||
| # } else | ||
| # return $this->render(array("action" => "edit")); | ||
| # } | ||
| # | ||
| def unvote | ||
| if !(story = Story.find_by_short_id(params[:story_id])) | ||
| return render :text => "can't find story", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(0, story.id, | ||
| nil, @user.id, nil) | ||
| render :text => "ok" | ||
| end | ||
| def upvote | ||
| if !(story = Story.find_by_short_id(params[:story_id])) | ||
| return render :text => "can't find story", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(1, story.id, | ||
| nil, @user.id, nil) | ||
| render :text => "ok" | ||
| end | ||
| def downvote | ||
| if !(story = Story.find_by_short_id(params[:story_id])) | ||
| return render :text => "can't find story", :status => 400 | ||
| end | ||
| if !Vote::STORY_REASONS[params[:reason]] | ||
| return render :text => "invalid reason", :status => 400 | ||
| end | ||
| Vote.vote_thusly_on_story_or_comment_for_user_because(-1, story.id, | ||
| nil, @user.id, params[:reason]) | ||
| render :text => "ok" | ||
| end | ||
| end |
| @@ -0,0 +1,51 @@ | ||
| 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")); | ||
| # } | ||
| end |
| @@ -0,0 +1,2 @@ | ||
| module ApplicationHelper | ||
| end |
| @@ -0,0 +1,11 @@ | ||
| class PasswordReset < ActionMailer::Base | ||
| default from: "nobody@lobste.rs" | ||
| def password_reset_link(user, ip) | ||
| @user = user | ||
| @ip = ip | ||
| mail(to: user.email, from: "Lobsters <nobody@lobste.rs", | ||
| subject: "[Lobsters] Reset your password") | ||
| end | ||
| end |
| @@ -0,0 +1,58 @@ | ||
| class Comment < ActiveRecord::Base | ||
| belongs_to :user | ||
| belongs_to :story | ||
| attr_accessible :comment | ||
| attr_accessor :parent_comment_short_id, :vote | ||
| def before_create | ||
| (1...10).each do |tries| | ||
| if tries == 10 | ||
| raise "too many hash collisions" | ||
| end | ||
| if !Comment.find_by_short_id(self.short_id = Utils.random_str(6)) | ||
| break | ||
| end | ||
| end | ||
| self.upvotes = 1 | ||
| end | ||
| def after_create | ||
| self.vote_for_user(self.user_id, 1) | ||
| self.story.upvote_comment_count! | ||
| end | ||
| def after_destroy | ||
| self.story.update_comment_count! | ||
| end | ||
| def score | ||
| self.upvotes - self.downvotes | ||
| end | ||
| def linkified_comment | ||
| Markdowner.markdown(self.comment) | ||
| end | ||
| def validate | ||
| self.comment.strip == "" && | ||
| self.errors.add(:comment, "cannot be blank.") | ||
| self.user_id.blank? && | ||
| self.errors.add(:user_id, "cannot be blank.") | ||
| self.story_id.blank? && | ||
| self.errors.add(:story_id, "cannot be blank.") | ||
| end | ||
| def upvote!(amount = 1) | ||
| Story.update_counters self.id, :upvotes => amount | ||
| end | ||
| def flag! | ||
| Story.update_counters self.id, :flaggings => 1 | ||
| end | ||
| end |
| @@ -0,0 +1,43 @@ | ||
| class Keystore < ActiveRecord::Base | ||
| validates_presence_of :key | ||
| attr_accessible nil | ||
| def self.get(key) | ||
| Keystore.find_by_key(key) | ||
| end | ||
| def self.put(key, value) | ||
| Keystore.connection.query([ "INSERT INTO #{Keystore.table_name} (" + | ||
| "`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `count` = ?", | ||
| key, amount ]) | ||
| true | ||
| end | ||
| def self.increment_value_for(key, amount = 1) | ||
| self.incremented_value_for(key, amount) | ||
| end | ||
| def self.incremented_value_for(key, amount = 1) | ||
| new_value = nil | ||
| Keystore.connection.query([ "INSERT INTO #{Keystore.table_name} (" + | ||
| "`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `count` = " + | ||
| "`count` + ?", key, amount, amount ]) | ||
| return self.value_for(key) | ||
| end | ||
| def self.decrement_value_for(key, amount = -1) | ||
| self.increment_value_for(key, amount) | ||
| end | ||
| def self.decremented_value_for(key, amount = -1) | ||
| self.incremented_value_for(key, amount) | ||
| end | ||
| def self.value_for(key) | ||
| self.get(key).try(:value) | ||
| end | ||
| end |
| @@ -0,0 +1,186 @@ | ||
| class Story < ActiveRecord::Base | ||
| belongs_to :user | ||
| has_many :taggings | ||
| has_many :comments | ||
| has_many :tags, :through => :taggings | ||
| 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 | ||
| 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 | ||
| def assign_short_id | ||
| (1...10).each do |tries| | ||
| if tries == 10 | ||
| raise "too many hash collisions" | ||
| end | ||
| self.short_id = Utils.random_str(6) | ||
| if !Story.find_by_short_id(self.short_id) | ||
| break | ||
| end | ||
| end | ||
| end | ||
| def deal_with_tags | ||
| self.tags_to_delete.each do |t| | ||
| if t.is_a?(Tagging) | ||
| t.destroy | ||
| elsif t.is_a?(Tag) | ||
| self.taggings.find_by_tag_id(t.id).try(:destroy) | ||
| end | ||
| end | ||
| self.tags_to_add.each do |t| | ||
| if t.is_a?(Tag) | ||
| tg = Tagging.new | ||
| tg.tag_id = t.id | ||
| tg.story_id = self.id | ||
| tg.save | ||
| end | ||
| end | ||
| self.tags_to_delete = [] | ||
| self.tags_to_add = [] | ||
| end | ||
| def comments_in_order_for_user(user_id) | ||
| Comment.find_all_by_story_id(self.id) | ||
| # TODO | ||
| end | ||
| def comments_url | ||
| "/p/#{self.short_id}/#{self.title_as_url}" | ||
| end | ||
| @_comment_count = nil | ||
| def comment_count | ||
| @_comment_count ||= | ||
| Keystore.value_for("story:#{self.id}:comment_count").to_i | ||
| end | ||
| def domain | ||
| if self.url.blank? | ||
| nil | ||
| else | ||
| pu = URI.parse(self.url) | ||
| pu.host.gsub(/^www\d*\./, "") | ||
| end | ||
| end | ||
| UP_RANGE = 400 | ||
| DOWN_RANGE = 100 | ||
| def hotness | ||
| score = upvotes - downvotes | ||
| order = Math.log([ score.abs, 1 ].max, 10) | ||
| if score > 0 | ||
| sign = 1 | ||
| elsif score < 0 | ||
| sign = -1 | ||
| else | ||
| sign = 0 | ||
| end | ||
| seconds = self.created_at.to_i - 398995200 | ||
| return -(order + (sign * (seconds.to_f / 45000))).round(7) | ||
| end | ||
| def linkified_text | ||
| Markdowner.markdown(self.description) | ||
| end | ||
| def tags_a | ||
| tags.map{|t| t.tag } | ||
| end | ||
| def tags_a=(new_tags) | ||
| self.tags_to_delete = [] | ||
| self.tags_to_add = [] | ||
| self.tags.each do |tag| | ||
| if !new_tags.include?(tag.tag) | ||
| self.tags_to_delete.push tag | ||
| end | ||
| end | ||
| new_tags.each do |tag| | ||
| if tag.to_s != "" && !self.tags.map{|t| t.tag }.include?(tag) | ||
| if t = Tag.find_by_tag(tag) | ||
| self.tags_to_add.push t | ||
| end | ||
| end | ||
| end | ||
| end | ||
| def title_as_url | ||
| u = self.title.downcase.gsub(/[^a-z0-9_-]/, "_") | ||
| while self.title.match(/__/) | ||
| self.title.gsub!("__", "_") | ||
| end | ||
| u | ||
| end | ||
| def url_or_comments_url | ||
| self.url.blank? ? self.comments_url : self.url | ||
| end | ||
| def is_editable_by_user?(user) | ||
| if !user || user.id != self.user_id | ||
| return false | ||
| end | ||
| true #(Time.now.to_i - self.created_at.to_i < (60 * Story::MAX_EDIT_MINS)) | ||
| end | ||
| def update_comment_count! | ||
| Keystore.put("story:#{self.id}:comment_count", | ||
| Comment.count_by_story_id(self.id)) | ||
| end | ||
| def validate | ||
| if self.title.blank? | ||
| self.errors.add(:title, "cannot be blank.") | ||
| end | ||
| # if (strlen($this->title) > 100) | ||
| # $this->errors->add("title", "cannot be longer than 100 " | ||
| # . "characters."); | ||
| # | ||
| # if ($this->story_type == "text") { | ||
| # $this->url = null; | ||
| # | ||
| # if (trim($this->description) == "") | ||
| # $this->errors->add("description", "cannot be blank."); | ||
| # elseif (strlen($this->description) > (64 * 1024)) | ||
| # $this->errors->add("description", "is too long."); | ||
| # } | ||
| # else { | ||
| # $this->description = null; | ||
| # | ||
| # if (!preg_match("/^https?:\/\//i", $this->url)) | ||
| # $this->errors->add("url", "does not look valid."); | ||
| # | ||
| # $now = new DateTime("now"); | ||
| # if (($old = Story::find_by_url($this->url)) && | ||
| # ($old->created_at->diff($now)->format("%s") < (60 * 60 * 30))) { | ||
| # $this->errors->add("url", "has already been posted in the " | ||
| # . "last 30 days."); | ||
| # $this->already_posted_story = $old; | ||
| # } | ||
| # } | ||
| # | ||
| # if (empty($this->user_id)) | ||
| # $this->errors->add("user_id", "cannot be blank."); | ||
| end | ||
| def flag! | ||
| Story.update_counters self.id, :flaggings => 1 | ||
| end | ||
| end |
| @@ -0,0 +1,2 @@ | ||
| class Tag < ActiveRecord::Base | ||
| end |
| @@ -0,0 +1,4 @@ | ||
| class Tagging < ActiveRecord::Base | ||
| belongs_to :tag | ||
| belongs_to :story | ||
| end |
| @@ -0,0 +1,40 @@ | ||
| class User < ActiveRecord::Base | ||
| has_many :stories, | ||
| :include => :user | ||
| has_secure_password | ||
| validates_format_of :username, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\Z/ | ||
| validates_uniqueness_of :username, :case_sensitive => false | ||
| validates_format_of :email, :with => /\A[^@]+@[^@]+\.[^@]+\Z/ | ||
| validates_uniqueness_of :email, :case_sensitive => false | ||
| validates_presence_of :password, :on => :create | ||
| attr_accessible :username, :email, :password, :password_confirmation, | ||
| :email_notifications | ||
| before_save :check_session_token | ||
| def check_session_token | ||
| if self.session_token.blank? | ||
| self.session_token = Utils.random_key(60) | ||
| end | ||
| end | ||
| def unread_message_count | ||
| 0 | ||
| #Message.where(:recipient_user_id => self.id, :has_been_read => 0).count | ||
| end | ||
| def karma | ||
| Keystore.value_for("user:#{self.id}:karma").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 | ||
| end |
| @@ -0,0 +1,102 @@ | ||
| class Vote < ActiveRecord::Base | ||
| belongs_to :user | ||
| belongs_to :story | ||
| STORY_REASONS = { | ||
| "S" => "Spam", | ||
| "T" => "Poorly Tagged", | ||
| "O" => "Off-topic", | ||
| "" => "Cancel", | ||
| } | ||
| COMMENT_REASONS = { | ||
| "O" => "Off-topic", | ||
| "I" => "Incorrect", | ||
| "T" => "Troll", | ||
| "S" => "Spam", | ||
| "" => "Cancel", | ||
| } | ||
| def self.votes_by_user_for_stories_hash(user, stories) | ||
| votes = [] | ||
| Vote.where(:user_id => user, :story_id => stories).each do |v| | ||
| votes[v.story_id] = v.vote | ||
| end | ||
| votes | ||
| end | ||
| def self.comment_votes_by_user_for_story_hash(user_id, story_id) | ||
| votes = {} | ||
| Vote.find(:all, :conditions => [ "user_id = ? AND story_id = ? AND " + | ||
| "comment_id IS NOT NULL", user_id, story_id ]).each do |v| | ||
| votes[v.comment_id] = cv.vote | ||
| end | ||
| votes | ||
| end | ||
| def self.vote_thusly_on_story_or_comment_for_user_because(vote, story_id, | ||
| comment_id, user_id, reason) | ||
| v = if story_id | ||
| Vote.find_or_initialize_by_user_id_and_story_id(user_id, story_id) | ||
| elsif comment_id | ||
| Vote.find_or_initialize_by_user_id_and_comment_id(user_id, comment_id) | ||
| end | ||
| if !v.new_record? && v.vote == vote | ||
| return | ||
| end | ||
| upvote = 0 | ||
| downvote = 0 | ||
| Vote.transaction do | ||
| # unvote | ||
| if vote == 0 | ||
| if !v.new_record? | ||
| if v.vote == -1 | ||
| downvote = -1 | ||
| else | ||
| upvote = -1 | ||
| end | ||
| end | ||
| v.destroy | ||
| # new vote or change vote | ||
| else | ||
| if v.new_record? | ||
| if v.vote == -1 | ||
| downvote = 1 | ||
| else | ||
| upvote = 1 | ||
| end | ||
| elsif v.vote == -1 | ||
| # changing downvote to upvote | ||
| downvote = -1 | ||
| upvote = 1 | ||
| elsif v.vote == 1 | ||
| # changing upvote to downvote | ||
| upvote = -1 | ||
| downvote = 1 | ||
| end | ||
| v.vote = vote | ||
| v.reason = reason | ||
| v.save! | ||
| end | ||
| if downvote != 0 || upvote != 0 | ||
| if v.story_id | ||
| Story.update_counters v.story_id, :downvotes => downvote, | ||
| :upvotes => upvote | ||
| elsif v.comment_id | ||
| Comment.update_counters v.comment_id, :downvotes => downvote, | ||
| :upvotes => upvote | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
| @@ -0,0 +1,2 @@ | ||
| <div id="footer"> | ||
| </div> |
| @@ -0,0 +1,43 @@ | ||
| <div id="header"> | ||
| <div id="headerright" class="<%= @user ? "loggedin" : "" %>"> | ||
| <% if @user %> | ||
| <a href="/users/<%= @user.username %>"><%= @user.username | ||
| %> (<%= @user.karma %>)</a> | ||
| <% if (count = @user.unread_message_count) > 0 %> | ||
| <a href="/messages"><%= count %> New Message<%= count == 1 ? "" : "s" | ||
| %></a> | ||
| <% else %> | ||
| <a href="/messages">Messages</a> | ||
| <% end %> | ||
| <%= link_to "Logout", { :controller => "login", :action => "logout" }, | ||
| { :confirm => "Are you sure you want to logout?", | ||
| "method" => "post" } %> | ||
| <% else %> | ||
| <a href="/signup">Signup</a> | ||
| or | ||
| <a href="/login">Login</a> | ||
| <% end %> | ||
| </div> | ||
| <div id="l_holder" class="boring"> | ||
| <a href="/"><img src="/assets/l.png" width=16 height=16></a> | ||
| </div> | ||
| <% if @title %> | ||
| <span id="headertitle"> | ||
| <a href="<%= @title_url %>"><%= @title %></a> | ||
| </span> | ||
| <% end %> | ||
| <span id="headerlinks"> | ||
| <a href="/">Home</a> | ||
| <a href="/newest">Newest</a> | ||
| <% if @user %> | ||
| <a href="/threads">Your Threads</a> | ||
| <% end %> | ||
| <a href="/stories/new">Submit</a> | ||
| </span> | ||
| <div class="clear"></div> | ||
| </div> |
| @@ -0,0 +1,10 @@ | ||
| <% 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 %> | ||
| </ol> |
| @@ -0,0 +1,28 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <meta http-equiv="content-type" content="text/html; charset=utf-8"> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||
| <title><%= @page_title ? "#{@page_title} | Lobsters" : "Lobsters" %></title> | ||
| <%= stylesheet_link_tag "application", :media => "all" %> | ||
| <%= javascript_include_tag "application" %> | ||
| <%= csrf_meta_tags %> | ||
| <script> | ||
| Lobsters.storyDownvoteReasons = { <%= raw Vote::STORY_REASONS.map{|k,v| | ||
| "#{k.inspect}: #{v.inspect}" }.join(", ") %> }; | ||
| Lobsters.commentDownvoteReasons = { <%= raw Vote::COMMENT_REASONS.map{|k,v| | ||
| "#{k.inspect}: #{v.inspect}" }.join(", ") %> }; | ||
| </script> | ||
| </head> | ||
| <body> | ||
| <div id="wrapper"> | ||
| <%= render :partial => "global/header" %> | ||
| <div id="inside"> | ||
| <%= yield %> | ||
| </div> | ||
| <%= render :partial => "global/footer" %> | ||
| </div> | ||
| </body> | ||
| </html> |
| @@ -0,0 +1,24 @@ | ||
| <div class="box"> | ||
| <div class="legend"> | ||
| Reset Password | ||
| </div> | ||
| <p> | ||
| If you've forgotten your password, enter your e-mail address or username | ||
| below and instructions will be e-mailed to you. | ||
| </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 /> | ||
| <p> | ||
| <%= submit_tag "Reset Password" %> | ||
| </p> | ||
| <% end %> | ||
| </div> |
| @@ -0,0 +1,36 @@ | ||
| <div class="box wide"> | ||
| <div class="legend"> | ||
| Login | ||
| </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 %> | ||
| <br /> | ||
| <%= label_tag :password, "Password:" %> | ||
| <%= password_field_tag :password, "", :size => 30 %> | ||
| <br /> | ||
| </p> | ||
| <p> | ||
| <%= submit_tag "Login" %> | ||
| </p> | ||
| <p> | ||
| Forgot your password? <%= link_to "Reset your password", | ||
| forgot_password_url %>. | ||
| </p> | ||
| <p> | ||
| Not signed up yet? <%= link_to "Signup", signup_url %>. | ||
| </p> | ||
| <% end %> | ||
| </div> |
| @@ -0,0 +1,32 @@ | ||
| <div class="box"> | ||
| <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] %> | ||
| <p> | ||
| <%= label_tag :username, "Username:" %> | ||
| <%= @reset_user.username %> | ||
| <br /> | ||
| <%= label_tag :password, "New Password:" %> | ||
| <%= password_field_tag :password, "", :size => 30 %> | ||
| <br /> | ||
| <%= label_tag :password_confirmation, "(Again):" %> | ||
| <%= password_field_tag :password_confirmation, "", :size => 30 %> | ||
| <br /> | ||
| <p> | ||
| <%= submit_tag "Set New Password" %> | ||
| </p> | ||
| <% end %> | ||
| </div> |
| @@ -0,0 +1,30 @@ | ||
| <table class="data" cellspacing=0 width="100%"> | ||
| <tr> | ||
| <? if (!empty($show_from)) { ?> | ||
| <th width="20%">From</th> | ||
| <? } ?> | ||
| <? if (!empty($show_to)) { ?> | ||
| <th width="20%">To</th> | ||
| <? } ?> | ||
| <th width="60%">Subject</th> | ||
| <th width="20%">Date</th> | ||
| </tr> | ||
| <? foreach ($messages as $message) { ?> | ||
| <tr <?= $message->has_been_read ? "" : "class=\"unread\"" ?>> | ||
| <? if ($show_from) { ?> | ||
| <td><?= $html->link_to(h($message->author->username), | ||
| array("controller" => "users", "action" => "show", | ||
| "id" => $message->author->username)); ?></td> | ||
| <? } ?> | ||
| <? if ($show_to) { ?> | ||
| <td><?= $html->link_to(h($message->recipient->username), | ||
| array("controller" => "users", "action" => "show", | ||
| "id" => $message->recipient->username)); ?></td> | ||
| <? } ?> | ||
| <td><strong><?= $html->link_to(h($message->subject), | ||
| array("controller" => "messages", "action" => "show", | ||
| "id" => $message->random_hash)); ?></strong></td> | ||
| <td><?= h($message->created_at->format("Y-m-d H:i:s")); ?></td> | ||
| </tr> | ||
| <? } ?> | ||
| </table> |
| @@ -0,0 +1,23 @@ | ||
| <div class="box noborder"> | ||
| <?= $html->error_messages_for($message); ?> | ||
| <? $form->form_for($message, array("controller" => "messages", | ||
| "action" => "send", "id" => $controller->recipient_user->username), array(), function($f) use | ||
| ($html, $controller) { ?> | ||
| <?= $f->label("recipient_user_id", "To:", array("class" => "required")); ?> | ||
| <?= $html->link_to(h($controller->message->recipient->username), | ||
| array("controller" => "users", "action" => "show", "id" => | ||
| $controller->message->recipient->username)); ?> | ||
| <br /> | ||
| <?= $f->label("subject", "Subject:", array("class" => "required")); ?> | ||
| <?= $f->text_field("subject", array("size" => 51)); ?> | ||
| <br /> | ||
| <?= $f->text_area("body", array("size" => "70x5")); ?> | ||
| <br /> | ||
| <p> | ||
| <?= $f->submit_tag("Send Message"); ?> | ||
| </p> | ||
| <? }); ?> | ||
| </div> |
| @@ -0,0 +1,13 @@ | ||
| <p> | ||
| <strong>Inbox</strong> | ||
| <?= $controller->render(array("partial" => "messages/list"), | ||
| array("messages" => $controller->incoming_messages, | ||
| "show_from" => true)); ?> | ||
| </p> | ||
| <p> | ||
| <strong>Sent Messages</strong> | ||
| <?= $controller->render(array("partial" => "messages/list"), | ||
| array("messages" => $controller->sent_messages, | ||
| "show_to" => true)); ?> | ||
| </p> |
| @@ -0,0 +1,69 @@ | ||
| <div> | ||
| <?= $html->link_to("« Back to Messages", | ||
| array("controller" => "messages")); ?> | ||
| </div> | ||
| <p> | ||
| <div class="box noborder"> | ||
| <label class="required">From:</label> | ||
| <?= $html->link_to(h($message->author->username), | ||
| array("controller" => "users", "action" => "show", | ||
| "id" => $message->author->username)); ?> | ||
| <br /> | ||
| <label class="required">To:</label> | ||
| <?= $html->link_to(h($message->recipient->username), | ||
| array("controller" => "users", "action" => "show", | ||
| "id" => $message->recipient->username)); ?> | ||
| <br /> | ||
| <label class="required">Date:</label> | ||
| <?= $message->created_at->format("D, j F Y \\a\\t H:i:s"); ?> | ||
| <br /> | ||
| <label class="required">Subject:</label> | ||
| <? if ($message->item) { ?> | ||
| <?= $html->link_to(h($message->subject), | ||
| array("controller" => "items", "action" => "show", | ||
| "id" => $message->item_id)); ?> | ||
| <? } else { ?> | ||
| <?= h($message->subject); ?> | ||
| <? } ?> | ||
| <br /> | ||
| <div> | ||
| <?= $message->sanitized_body(); ?> | ||
| </div> | ||
| <div class="clear"></div> | ||
| <div class="s"></div> | ||
| <p> | ||
| <strong><a href="#" onclick="Element.show('reply'); return false;">Send a | ||
| Reply</a></strong> | ||
| <div id="reply" style="display: none;"> | ||
| <p> | ||
| <?= $html->error_messages_for($reply); ?> | ||
| <? $form->form_for($reply, array("controller" => "messages", | ||
| "action" => "reply", "id" => $message->random_hash), array(), function($f) use | ||
| ($html, $controller) { ?> | ||
| <?= $f->label("recipient_user_id", "To:", array("class" => "required")); ?> | ||
| <?= $html->link_to(h($controller->message->recipient->username), | ||
| array("controller" => "users", "action" => "show", "id" => | ||
| $controller->message->recipient->username)); ?> | ||
| <br /> | ||
| <?= $f->label("subject", "Subject:", array("class" => "required")); ?> | ||
| <?= h($controller->reply->subject); ?> | ||
| <br /> | ||
| <?= $f->text_area("body", array("size" => "70x5")); ?> | ||
| <br /> | ||
| <p> | ||
| <?= $f->submit_tag("Send Reply"); ?> | ||
| </p> | ||
| <? }); ?> | ||
| </p> | ||
| </div> | ||
| </div> |
| @@ -0,0 +1,7 @@ | ||
| Hello <%= @user.email %>, | ||
| Someone at <%= @ip %> requested to reset your account password. | ||
| If you submitted this request, visit the link below to set a new password. | ||
| If not, you can disregard this e-mail. | ||
| http://lobste.rs/login/set_new_password?token=<%= @user.password_reset_token %> |
| @@ -0,0 +1,42 @@ | ||
| <div class="box"> | ||
| <div class="legend"> | ||
| Create an Account | ||
| </div> | ||
| <%= form_for @new_user, { :url => signup_url, | ||
| :autocomplete => "off" } do |f| %> | ||
| <p> | ||
| To create a new account, enter your e-mail address and a password. | ||
| Your e-mail address will never be shown to users and will only be used | ||
| if you need to reset your password, or to receive optional new-message | ||
| alerts. | ||
| </p> | ||
| <%= error_messages_for(@new_user) %> | ||
| <p> | ||
| <%= f.label :username, "Username:" %> | ||
| <%= f.text_field :username, :size => 30 %> | ||
| <span class="hint"> | ||
| <tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt> | ||
| </span> | ||
| <br /> | ||
| <%= f.label :email, "E-mail Address:" %> | ||
| <%= f.email_field :email, :size => 30 %> | ||
| <br /> | ||
| <%= f.label :password, "Password:" %> | ||
| <%= f.password_field :password, :size => 30 %> | ||
| <br /> | ||
| <%= f.label :password_confirmation, "Password (again):" %> | ||
| <%= f.password_field :password_confirmation, :size => 30 %> | ||
| <br /> | ||
| </p> | ||
| <p> | ||
| <%= submit_tag "Signup" %> | ||
| </p> | ||
| <% end %> | ||
| </fieldset> |
| @@ -0,0 +1,41 @@ | ||
| <li id="comment_<%= comment.short_id %>" class="<%= comment.vote == 1 ? | ||
| "upvoted" : (comment.vote == -1 ? "downvoted" : "") %> | ||
| <%= comment.score <= 0 ? "negative" : "" %> | ||
| <%= comment.score <= -3 ? "negative_3" : "" %> | ||
| <%= comment.score <= -5 ? "negative_5" : "" %> | ||
| <%= comment.score <= -7 ? "negative_7" : "" %> | ||
| "> | ||
| <div class="voters"> | ||
| <a class="upvoter" href="#" onclick="<%= comment.id ? | ||
| "Lobsters.upvoteComment('#{comment.short_id}'); " : "" %>return false;" | ||
| ></a> | ||
| <div class="score"> | ||
| <%= comment.score %> | ||
| </div> | ||
| <a class="downvoter" id="comment_downvoter_<%= comment.short_id %>" | ||
| href="#" onclick="<%= comment.id ? | ||
| "Lobsters.downvoteComment('#{comment.short_id}'); " : "" %>return false;" | ||
| ></a> | ||
| </div> | ||
| <div class="details"> | ||
| <div class="byline"> | ||
| <a href="/u/<%= comment.user.username %>"><%= comment.user.username | ||
| %></a> | ||
| <% if comment.created_at %> | ||
| <%= time_ago_in_words(comment.created_at).gsub(/^about /, "") %> ago | ||
| <% else %> | ||
| just now | ||
| <% end %> | ||
| </div> | ||
| <div class="comment_text"> | ||
| <%= raw comment.linkified_comment %> | ||
| <div class="comment_actions"> | ||
| <a href="<%= story.comments_url %>/comments/<%= comment.short_id | ||
| %>">link</a> | ||
| | ||
| <%= link_to("reply") %> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </li> |
| @@ -0,0 +1,23 @@ | ||
| <%= form_tag({ :controller => "comments", :action => "create", | ||
| :story_id => story.short_id }, :method => :post) do |f| %> | ||
| <%= text_area_tag "comment", comment.comment, :size => "100x5" %> | ||
| <p></p> | ||
| <%= button_to_function "Post Comment", | ||
| "Lobsters.postComment($(this).parents('form').first()); return false;", | ||
| :type => "submit" %> | ||
| | ||
| <%= button_to_function "Preview Comment", | ||
| "Lobsters.previewComment($(this).parents('form').first()); return false;" | ||
| %> | ||
| <% if params[:preview].present? %> | ||
| <ol class="comments"> | ||
| <%= render :partial => "stories/comment", | ||
| :locals => { :comment => comment, :story => story } %> | ||
| </ol> | ||
| <% end %> | ||
| <% end %> |
| @@ -0,0 +1,84 @@ | ||
| <%= error_messages_for f.object %> | ||
| <div class="box"> | ||
| <div class="boxline"> | ||
| <% if !f.object.new_record? && !f.object.url.blank? %> | ||
| <label>URL:</label> | ||
| <span class="d"> | ||
| <%= f.object.url %> | ||
| </span> | ||
| <% elsif !f.object.id %> | ||
| <%= f.label :url, "URL:", :class => "required" %> | ||
| <%= f.text_field :url, :style => "width: 475px;" %> | ||
| <%= button_to_function "Fetch Title", | ||
| "Lobsters.fetchURLTitle($(this), $('#story_url'), $('#story_title'));" %> | ||
| <% end %> | ||
| </div> | ||
| <div class="boxline"> | ||
| <%= f.label :title, "Title:", :class => "required" %> | ||
| <%= f.text_field :title, :maxlength => 100, :style => "width: 475px;" %> | ||
| </div> | ||
| <div class="boxline" style="margin-bottom: 2px;"> | ||
| <%= f.label :tags_a, "Tags:", :class => "required" %> | ||
| <%= f.select "tags_a", options_for_select(Tag.order(:tag).map{|t| | ||
| [ "#{t.tag} - #{t.description}", t.tag ] }), {}, | ||
| { :multiple => true, :style => "width: 487px;" } %> | ||
| </div> | ||
| <div class="boxline"> | ||
| <%= f.label :description, "Text:", :class => "required" %> | ||
| <%= f.text_area :description, :size => "100x10", | ||
| :placeholder => "optional, not recommended when submitting a link" %> | ||
| </div> | ||
| <div class="hintblock" style="padding-bottom: 1em; font-style: normal;"> | ||
| <a href="#" style="color: gray;" | ||
| onclick="$('#markdown_help').toggle(); return false;"> | ||
| Limited Markdown formatting available | ||
| </a> | ||
| <div id="markdown_help" | ||
| style="display: none; padding-top: 0.5em;"> | ||
| <table> | ||
| <tr> | ||
| <td width="125"><em>emphasized text</em></td> | ||
| <td>surround text with *asterisks*</td> | ||
| </tr> | ||
| <tr> | ||
| <td><u>underlined text</u></td> | ||
| <td>surround text with _underline_</td> | ||
| </tr> | ||
| <tr> | ||
| <td><strike>struck-through</strike></td> | ||
| <td>surround text with ~~two tildes~~</td> | ||
| </tr> | ||
| <tr> | ||
| <td><a href="http://example.com/" | ||
| style="color: inherit;">linked text</a></td> | ||
| <td>[linked text](http://example.com/) or just a bare URL | ||
| to create without a title</td> | ||
| </tr> | ||
| <tr> | ||
| <td><blockquote> quoted text</blockquote></td> | ||
| <td>prefix text with ></td> | ||
| </tr> | ||
| <tr> | ||
| <td><pre style="margin: 0px;">pre | ||
| text</pre></td> | ||
| <td>prefix text with at least 3 spaces</td> | ||
| </tr> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <script type="text/javascript"> | ||
| $(document).ready(function() { | ||
| $("#story_tags_a").select2({ | ||
| formatSelection: function(what) { | ||
| return what.id; | ||
| } | ||
| }); | ||
| }); | ||
| </script> |
| @@ -0,0 +1,55 @@ | ||
| <li id="story_<%= story.short_id %>" class="story <%= story.vote == 1 ? | ||
| "upvoted" : (story.vote == -1 ? "downvoted" : "") %> | ||
| <%= story.is_expired? ? "expired" : "" %>"> | ||
| <div class="voters"> | ||
| <% if @user %> | ||
| <a class="upvoter" href="#" | ||
| onclick="Lobsters.upvote('<%= story.short_id %>'); return false;" | ||
| ></a> | ||
| <% else %> | ||
| <%= link_to "", login_url, :class => "upvoter" %> | ||
| <% end %> | ||
| <div class="score"> | ||
| <%= story.upvotes %> | ||
| </div> | ||
| <% if @user %> | ||
| <a class="downvoter" href="#" id="story_downvoter_<%= story.short_id %>" | ||
| onclick="Lobsters.downvote('<%= story.short_id %>'); return false;" | ||
| ></a> | ||
| <% else %> | ||
| <%= link_to "", login_url, :class => "downvoter" %> | ||
| <% end %> | ||
| </div> | ||
| <div class="details"> | ||
| <span class="link"> | ||
| <%= link_to story.title, story.url_or_comments_url %> | ||
| </span> | ||
| <span class="tags"> | ||
| <% story.taggings.each do |tagging| %> | ||
| <%= link_to tagging.tag.tag, tag_url(tagging.tag.tag), | ||
| :class => "tag tag_#{tagging.tag.tag}" %> | ||
| <% end %> | ||
| </span> | ||
| <span class="domain"> | ||
| <%= story.domain %> | ||
| </span> | ||
| <div class="byline"> | ||
| by <%= link_to story.user.username, :controller => "u", | ||
| :id => story.user.username %> | ||
| <%= time_ago_in_words(story.created_at).gsub(/^about /, "") %> ago | ||
| <% if story.is_editable_by_user? @user %> | ||
| | | ||
| <%= link_to "edit", :controller => "stories", :action => "edit", | ||
| :id => story.short_id %> | ||
| | | ||
| <%= link_to "delete", url_for({ :controller => "stories", | ||
| :action => "delete", :id => story.short_id }), | ||
| :confirm => "Are you sure you want to delete this story?" %> | ||
| <% end %> | ||
| | | ||
| <%= link_to((c = story.comment_count) == 0 ? "discuss" : | ||
| "#{c} comment#{c == 1 ? "" : "s"}", story.comments_url) %> | ||
| </div> | ||
| </div> | ||
| </li> |
| @@ -0,0 +1,13 @@ | ||
| <h1>Edit a Story</h1> | ||
| <? $form->form_for($story, array("controller" => "stories", | ||
| "action" => "update", "id" => $item->short_id), array(), | ||
| function($f) use ($C) { ?> | ||
| <?= $C->render(array("partial" => "stories/form"), | ||
| array("f" => $f)); ?> | ||
| <div class="box_submitter"> | ||
| <?= $f->submit_tag("Save Changes"); ?> | ||
| </div> | ||
| <? }); ?> | ||
| </div> |
| @@ -0,0 +1,2 @@ | ||
| <?= $controller->render(array("partial" => "items/list"), | ||
| array("items" => $controller->items, "show_caption" => true)); ?> |
| @@ -0,0 +1,15 @@ | ||
| <div class="box"> | ||
| <div class="legend"> | ||
| Submit a Story | ||
| </div> | ||
| <%= form_for @story do |f| %> | ||
| <%= render :partial => "stories/form", :locals => { :story => @story, | ||
| :f => f } %> | ||
| <p></p> | ||
| <div class="box"> | ||
| <%= submit_tag "Submit" %> | ||
| </div> | ||
| <% end %> | ||
| </div> |
| @@ -0,0 +1,23 @@ | ||
| <ol class="stories"> | ||
| <%= render :partial => "stories/listdetail", | ||
| :locals => { :story => @story } %> | ||
| </ol> | ||
| <div class="story_content"> | ||
| <% if @story.url.blank? %> | ||
| <div class="story_text"> | ||
| <%= raw @story.linkified_text %> | ||
| </div> | ||
| <% end %> | ||
| <p></p> | ||
| <% if @user %> | ||
| <%= render :partial => "stories/commentbox", | ||
| :locals => { :story => @story, :comment => @comment } %> | ||
| <% end %> | ||
| </div> | ||
| <ol class="comments"> | ||
| <%= render :partial => "stories/comment", :locals => { :story => @story }, | ||
| :collection => @story.comments %> | ||
| </ol> |
| @@ -0,0 +1,51 @@ | ||
| <div style="float: right;"> | ||
| <strong> | ||
| <?= $html->link_to("Manage Your Items", | ||
| array("controller" => "items", "action" => "manage")); ?> | ||
| | | ||
| <?= $html->link_to("List a New Item", | ||
| array("controller" => "items", "action" => "build")); ?> | ||
| </strong> | ||
| </div> | ||
| <h2><?= h($showing_user->username); ?></h2> | ||
| <div class="h2desc"> | ||
| a user for <?= $time->time_ago_in_words($showing_user->created_at); ?> | ||
| </div> | ||
| <p> | ||
| <div class="box noborder"> | ||
| <? $form->form_for($showing_user, array("controller" => "users", | ||
| "action" => "update"), array("autocomplete" => "off"), function($f) | ||
| use ($html) { ?> | ||
| <?= $html->error_messages_for($f->form_object); ?> | ||
| <?= $f->label("username", "Username:", array("class" => "required")); ?> | ||
| <span><?= h($f->form_object->username); ?></span> | ||
| <br /> | ||
| <?= $f->label("new_password", "New Password:"); ?> | ||
| <?= $f->password_field("new_password", array("size" => 20)); ?> | ||
| <br /> | ||
| <?= $f->label("new_password_confirmation", "Password (Again):"); ?> | ||
| <?= $f->password_field("new_password_confirmation", array("size" => 20)); ?> | ||
| <br /> | ||
| <?= $f->label("email", "E-Mail Address:", array("class" => "required")); ?> | ||
| <?= $f->text_field("email", array("size" => 20)); ?> | ||
| <br /> | ||
| <label class="required">Settings:</label> | ||
| <?= $f->check_box("email_notifications"); ?> | ||
| <?= $f->label("email_notifications", "Receive E-Mail Notifications For " | ||
| . "New Messages", array("class" => "norm")); ?> | ||
| <br /> | ||
| <p> | ||
| <?= $f->submit_tag("Save Settings"); ?> | ||
| </p> | ||
| <? }); ?> | ||
| </div> | ||
| </p> |
| @@ -0,0 +1,24 @@ | ||
| <h2><?= h($showing_user->username); ?></h2> | ||
| <div class="h2desc"> | ||
| a user for <?= $time->time_ago_in_words($showing_user->created_at); ?> | ||
| </div> | ||
| <p> | ||
| <? if (count($items->collection)) { ?> | ||
| <?= $controller->render(array("partial" => "items/list"), | ||
| array("items" => $items)); ?> | ||
| <? } else { ?> | ||
| <strong>No Items Listed</strong> | ||
| <? } ?> | ||
| </p> | ||
| <p> | ||
| <strong>Contact</strong><br /> | ||
| <ul> | ||
| <li><?= $html->link_to("Send Message", | ||
| array("controller" => "messages", "action" => "compose", "id" => | ||
| $showing_user->username)); ?> | ||
| <li><?= $html->link_to("View Hacker News Profile", | ||
| "http://news.ycombinator.com/user?id=" . h($showing_user->username)); ?> | ||
| </ul> | ||
| </p> |
| @@ -0,0 +1,4 @@ | ||
| # This file is used by Rack-based servers to start the application. | ||
| require ::File.expand_path('../config/environment', __FILE__) | ||
| run Lobsters::Application |
| @@ -0,0 +1,59 @@ | ||
| require File.expand_path('../boot', __FILE__) | ||
| require 'rails/all' | ||
| if defined?(Bundler) | ||
| # If you precompile assets before deploying to production, use this line | ||
| Bundler.require(*Rails.groups(:assets => %w(development test))) | ||
| # If you want your assets lazily compiled in production, use this line | ||
| # Bundler.require(:default, :assets, Rails.env) | ||
| end | ||
| module Lobsters | ||
| class Application < Rails::Application | ||
| # Settings in config/environments/* take precedence over those specified here. | ||
| # Application configuration should go into files in config/initializers | ||
| # -- all .rb files in that directory are automatically loaded. | ||
| # Custom directories with classes and modules you want to be autoloadable. | ||
| config.autoload_paths += %W(#{config.root}/extras) | ||
| # Only load the plugins named here, in the order given (default is alphabetical). | ||
| # :all can be used as a placeholder for all plugins not explicitly named. | ||
| # config.plugins = [ :exception_notification, :ssl_requirement, :all ] | ||
| # Activate observers that should always be running. | ||
| # config.active_record.observers = :cacher, :garbage_collector, :forum_observer | ||
| # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. | ||
| # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. | ||
| # config.time_zone = 'Central Time (US & Canada)' | ||
| # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. | ||
| # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] | ||
| # config.i18n.default_locale = :de | ||
| # Configure the default encoding used in templates for Ruby 1.9. | ||
| config.encoding = "utf-8" | ||
| # Configure sensitive parameters which will be filtered from the log file. | ||
| config.filter_parameters += [:password] | ||
| # Use SQL instead of Active Record's schema dumper when creating the database. | ||
| # This is necessary if your schema can't be completely dumped by the schema dumper, | ||
| # like if you have constraints or database-specific column types | ||
| # config.active_record.schema_format = :sql | ||
| # Enforce whitelist mode for mass assignment. | ||
| # This will create an empty whitelist of attributes available for mass-assignment for all models | ||
| # in your app. As such, your models will need to explicitly whitelist or blacklist accessible | ||
| # parameters by using an attr_accessible or attr_protected declaration. | ||
| # config.active_record.whitelist_attributes = true | ||
| # Enable the asset pipeline | ||
| config.assets.enabled = true | ||
| # Version of your assets, change this if you want to expire all your assets | ||
| config.assets.version = '1.0' | ||
| end | ||
| end |
| @@ -0,0 +1,6 @@ | ||
| require 'rubygems' | ||
| # Set up gems listed in the Gemfile. | ||
| ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) | ||
| require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) |
| @@ -0,0 +1,5 @@ | ||
| # Load the rails application | ||
| require File.expand_path('../application', __FILE__) | ||
| # Initialize the rails application | ||
| Lobsters::Application.initialize! |
| @@ -0,0 +1,37 @@ | ||
| Lobsters::Application.configure do | ||
| # Settings specified here will take precedence over those in config/application.rb | ||
| # In the development environment your application's code is reloaded on | ||
| # every request. This slows down response time but is perfect for development | ||
| # since you don't have to restart the web server when you make code changes. | ||
| config.cache_classes = false | ||
| # Log error messages when you accidentally call methods on nil. | ||
| config.whiny_nils = true | ||
| # Show full error reports and disable caching | ||
| config.consider_all_requests_local = true | ||
| config.action_controller.perform_caching = false | ||
| # Don't care if the mailer can't send | ||
| config.action_mailer.raise_delivery_errors = false | ||
| # Print deprecation notices to the Rails logger | ||
| config.active_support.deprecation = :log | ||
| # Only use best-standards-support built into browsers | ||
| config.action_dispatch.best_standards_support = :builtin | ||
| # Raise exception on mass assignment protection for Active Record models | ||
| config.active_record.mass_assignment_sanitizer = :strict | ||
| # Log the query plan for queries taking more than this (works | ||
| # with SQLite, MySQL, and PostgreSQL) | ||
| config.active_record.auto_explain_threshold_in_seconds = 0.5 | ||
| # Do not compress assets | ||
| config.assets.compress = false | ||
| # Expands the lines which load the assets | ||
| config.assets.debug = true | ||
| end |
| @@ -0,0 +1,67 @@ | ||
| Lobsters::Application.configure do | ||
| # Settings specified here will take precedence over those in config/application.rb | ||
| # Code is not reloaded between requests | ||
| config.cache_classes = true | ||
| # Full error reports are disabled and caching is turned on | ||
| config.consider_all_requests_local = false | ||
| config.action_controller.perform_caching = true | ||
| # Disable Rails's static asset server (Apache or nginx will already do this) | ||
| config.serve_static_assets = false | ||
| # Compress JavaScripts and CSS | ||
| config.assets.compress = true | ||
| # Don't fallback to assets pipeline if a precompiled asset is missed | ||
| config.assets.compile = false | ||
| # Generate digests for assets URLs | ||
| config.assets.digest = true | ||
| # Defaults to Rails.root.join("public/assets") | ||
| # config.assets.manifest = YOUR_PATH | ||
| # Specifies the header that your server uses for sending files | ||
| # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache | ||
| # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx | ||
| # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. | ||
| # config.force_ssl = true | ||
| # See everything in the log (default is :info) | ||
| # config.log_level = :debug | ||
| # Prepend all log lines with the following tags | ||
| # config.log_tags = [ :subdomain, :uuid ] | ||
| # Use a different logger for distributed setups | ||
| # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) | ||
| # Use a different cache store in production | ||
| # config.cache_store = :mem_cache_store | ||
| # Enable serving of images, stylesheets, and JavaScripts from an asset server | ||
| # config.action_controller.asset_host = "http://assets.example.com" | ||
| # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) | ||
| # config.assets.precompile += %w( search.js ) | ||
| # Disable delivery errors, bad email addresses will be ignored | ||
| # config.action_mailer.raise_delivery_errors = false | ||
| # Enable threaded mode | ||
| # config.threadsafe! | ||
| # Enable locale fallbacks for I18n (makes lookups for any locale fall back to | ||
| # the I18n.default_locale when a translation can not be found) | ||
| config.i18n.fallbacks = true | ||
| # Send deprecation notices to registered listeners | ||
| config.active_support.deprecation = :notify | ||
| # Log the query plan for queries taking more than this (works | ||
| # with SQLite, MySQL, and PostgreSQL) | ||
| # config.active_record.auto_explain_threshold_in_seconds = 0.5 | ||
| end |
| @@ -0,0 +1,37 @@ | ||
| Lobsters::Application.configure do | ||
| # Settings specified here will take precedence over those in config/application.rb | ||
| # The test environment is used exclusively to run your application's | ||
| # test suite. You never need to work with it otherwise. Remember that | ||
| # your test database is "scratch space" for the test suite and is wiped | ||
| # and recreated between test runs. Don't rely on the data there! | ||
| config.cache_classes = true | ||
| # Configure static asset server for tests with Cache-Control for performance | ||
| config.serve_static_assets = true | ||
| config.static_cache_control = "public, max-age=3600" | ||
| # Log error messages when you accidentally call methods on nil | ||
| config.whiny_nils = true | ||
| # Show full error reports and disable caching | ||
| config.consider_all_requests_local = true | ||
| config.action_controller.perform_caching = false | ||
| # Raise exceptions instead of rendering exception templates | ||
| config.action_dispatch.show_exceptions = false | ||
| # Disable request forgery protection in test environment | ||
| config.action_controller.allow_forgery_protection = false | ||
| # Tell Action Mailer not to deliver emails to the real world. | ||
| # The :test delivery method accumulates sent emails in the | ||
| # ActionMailer::Base.deliveries array. | ||
| config.action_mailer.delivery_method = :test | ||
| # Raise exception on mass assignment protection for Active Record models | ||
| config.active_record.mass_assignment_sanitizer = :strict | ||
| # Print deprecation notices to the stderr | ||
| config.active_support.deprecation = :stderr | ||
| end |
| @@ -0,0 +1,7 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
| # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. | ||
| # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } | ||
| # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. | ||
| # Rails.backtrace_cleaner.remove_silencers! |
| @@ -0,0 +1,15 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
| # Add new inflection rules using the following format | ||
| # (all these examples are active by default): | ||
| # ActiveSupport::Inflector.inflections do |inflect| | ||
| # inflect.plural /^(ox)$/i, '\1en' | ||
| # inflect.singular /^(ox)en/i, '\1' | ||
| # inflect.irregular 'person', 'people' | ||
| # inflect.uncountable %w( fish sheep ) | ||
| # end | ||
| # | ||
| # These inflection rules are supported but not enabled by default: | ||
| # ActiveSupport::Inflector.inflections do |inflect| | ||
| # inflect.acronym 'RESTful' | ||
| # end |
| @@ -0,0 +1,5 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
| # Add new mime types for use in respond_to blocks: | ||
| # Mime::Type.register "text/richtext", :rtf | ||
| # Mime::Type.register_alias "text/html", :iphone |
| @@ -0,0 +1,9 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
| Lobsters::Application.config.session_store :cookie_store, | ||
| key: 'lobster_trap', expire_after: 1.month | ||
| # Use the database for sessions instead of the cookie-based default, | ||
| # which shouldn't be used to store highly confidential information | ||
| # (create the session table with "rails generate session_migration") | ||
| # Lobsters::Application.config.session_store :active_record_store |
| @@ -0,0 +1,14 @@ | ||
| # Be sure to restart your server when you modify this file. | ||
| # | ||
| # This file contains settings for ActionController::ParamsWrapper which | ||
| # is enabled by default. | ||
| # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. | ||
| ActiveSupport.on_load(:action_controller) do | ||
| wrap_parameters format: [:json] | ||
| end | ||
| # Disable root element in JSON by default. | ||
| ActiveSupport.on_load(:active_record) do | ||
| self.include_root_in_json = false | ||
| end |
| @@ -0,0 +1,5 @@ | ||
| # Sample localization file for English. Add more files in this directory for other locales. | ||
| # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. | ||
| en: | ||
| hello: "Hello world" |
| @@ -0,0 +1,35 @@ | ||
| Lobsters::Application.routes.draw do | ||
| root :to => "home#index" | ||
| 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" | ||
| post "/login/reset_password" => "login#reset_password", | ||
| :as => "reset_password" | ||
| match "/login/set_new_password" => "login#set_new_password", | ||
| :as => "set_new_password" | ||
| match "/t/:tag" => "home#tagged", :as => "tag" | ||
| resources :stories do | ||
| post "upvote" | ||
| post "downvote" | ||
| post "unvote" | ||
| end | ||
| post "/stories/fetch_url_title" => "stories#fetch_url_title" | ||
| resources :comments do | ||
| post "upvote" | ||
| post "downvote" | ||
| post "unvote" | ||
| end | ||
| post "/comments/preview" | ||
| get "/p/:id/:title" => "stories#show" | ||
| end |
| @@ -0,0 +1,100 @@ | ||
| # encoding: UTF-8 | ||
| # This file is auto-generated from the current state of the database. Instead | ||
| # of editing this file, please use the migrations feature of Active Record to | ||
| # incrementally modify your database, and then regenerate this schema definition. | ||
| # | ||
| # Note that this schema.rb definition is the authoritative source for your | ||
| # database schema. If you need to create the application database on another | ||
| # system, you should be using db:schema:load, not running all the migrations | ||
| # from scratch. The latter is a flawed and unsustainable approach (the more migrations | ||
| # you'll amass, the slower it'll run and the greater likelihood for issues). | ||
| # | ||
| # It's strongly recommended to check this file into your version control system. | ||
| ActiveRecord::Schema.define(:version => 0) do | ||
| create_table "comments", :force => true do |t| | ||
| t.datetime "created_at", :null => false | ||
| t.datetime "updated_at" | ||
| t.string "short_id", :limit => 10, :default => "", :null => false | ||
| t.integer "story_id", :null => false | ||
| t.integer "user_id", :null => false | ||
| t.integer "parent_comment_id" | ||
| t.text "comment", :null => false | ||
| t.integer "upvotes", :default => 0, :null => false | ||
| t.integer "downvotes", :default => 0, :null => false | ||
| end | ||
| add_index "comments", ["story_id", "short_id"], :name => "story_id" | ||
| create_table "keystores", :primary_key => "key", :force => true do |t| | ||
| t.integer "value", :null => false | ||
| end | ||
| create_table "messages", :force => true do |t| | ||
| t.datetime "created_at" | ||
| t.integer "author_user_id" | ||
| t.integer "recipient_user_id" | ||
| t.integer "has_been_read", :limit => 1, :default => 0 | ||
| t.string "subject", :limit => 100 | ||
| t.text "body" | ||
| t.integer "item_id" | ||
| t.string "random_hash", :limit => 30 | ||
| end | ||
| add_index "messages", ["random_hash"], :name => "random_hash", :unique => true | ||
| create_table "stories", :force => true do |t| | ||
| t.datetime "created_at" | ||
| t.integer "user_id" | ||
| t.string "url", :limit => 250, :default => "" | ||
| t.string "title", :limit => 100, :default => "", :null => false | ||
| t.text "description" | ||
| t.string "short_id", :limit => 6, :default => "", :null => false | ||
| t.integer "is_expired", :limit => 1, :default => 0, :null => false | ||
| t.integer "upvotes", :default => 0, :null => false | ||
| t.integer "downvotes", :default => 0, :null => false | ||
| end | ||
| add_index "stories", ["url"], :name => "url" | ||
| create_table "taggings", :force => true do |t| | ||
| t.integer "story_id", :null => false | ||
| t.integer "tag_id", :null => false | ||
| end | ||
| add_index "taggings", ["story_id", "tag_id"], :name => "story_id2", :unique => true | ||
| create_table "tags", :force => true do |t| | ||
| t.string "tag", :limit => 25, :default => "", :null => false | ||
| t.string "description", :limit => 100 | ||
| end | ||
| add_index "tags", ["tag"], :name => "tag", :unique => true | ||
| create_table "users", :force => true do |t| | ||
| t.string "username", :limit => 50 | ||
| t.string "email", :limit => 100 | ||
| t.string "password_digest", :limit => 75 | ||
| t.datetime "created_at" | ||
| t.integer "email_notifications", :limit => 1, :default => 1 | ||
| t.integer "is_admin", :limit => 1, :default => 0, :null => false | ||
| t.string "password_reset_token", :limit => 75 | ||
| t.string "session_token", :limit => 75, :default => "", :null => false | ||
| end | ||
| add_index "users", ["session_token"], :name => "session_hash", :unique => true | ||
| add_index "users", ["username"], :name => "username", :unique => true | ||
| create_table "votes", :force => true do |t| | ||
| t.integer "user_id", :null => false | ||
| t.integer "story_id", :null => false | ||
| t.integer "comment_id" | ||
| t.integer "vote", :limit => 1, :null => false | ||
| t.string "reason", :limit => 1 | ||
| end | ||
| add_index "votes", ["user_id", "comment_id"], :name => "user_id_2" | ||
| add_index "votes", ["user_id", "story_id"], :name => "user_id" | ||
| end |
| @@ -0,0 +1,7 @@ | ||
| # This file should contain all the record creation needed to seed the database with its default values. | ||
| # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). | ||
| # | ||
| # Examples: | ||
| # | ||
| # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) | ||
| # Mayor.create(name: 'Emanuel', city: cities.first) |
| @@ -0,0 +1,144 @@ | ||
| class Markdowner | ||
| MAX_BARE_LINK = 50 | ||
| def self.h(str) | ||
| # .to_str is needed because this becomes a SafeBuffer, which breaks gsub | ||
| # https://github.com/rails/rails/issues/1555 | ||
| ERB::Util.h(str).to_str.gsub(/<(\/?)(em|u|strike)>/, '<\1\2>') | ||
| end | ||
| def self.markdown(string) | ||
| lines = string.rstrip.split(/\r?\n/) | ||
| out = "<p>" | ||
| inpre = false | ||
| lines.each do |line| | ||
| # [ ][ ]blah -> <pre> blah</pre> | ||
| if line.match(/^( |\t)/) | ||
| if !inpre | ||
| out << "<p><pre>" | ||
| end | ||
| out << ERB::Util.h(line) << "\n" | ||
| inpre = true | ||
| next | ||
| elsif inpre | ||
| out << "</pre></p>\n<p>" | ||
| inpre = false | ||
| end | ||
| line = self.h(line) | ||
| # lines starting with > are quoted | ||
| if m = line.match(/^>(.*)/) | ||
| line = "<blockquote> " << m[1] << " </blockquote>" | ||
| end | ||
| lead = '\A|\s|[><]' | ||
| trail = '[<>]|\z|\s' | ||
| # *text* -> <em>text</em> | ||
| line.gsub!(/(#{lead}|_|~)\*([^\* \t][^\*]*)\*(#{trail}|_|~)/) do |m| | ||
| "#{$1}<em>" << self.h($2) << "</em>#{$3}" | ||
| end | ||
| # _text_ -> <u>text</u> | ||
| line.gsub!(/(#{lead}|~)_([^_ \t][^_]*)_(#{trail}|~)/) do |m| | ||
| "#{$1}<u>" << self.h($2) << "</u>#{$3}" | ||
| end | ||
| # ~~text~~ -> <strike>text</strike> (from reddit) | ||
| line.gsub!(/(#{lead})\~\~([^~ \t][^~]*)\~\~(#{trail})/) do |m| | ||
| "#{$1}<strike>" << self.h($2) << "</strike>#{$3}" | ||
| end | ||
| # [link text](http://url) -> <a href="http://url">link text</a> | ||
| line.gsub!(/(#{lead})\[([^\]]+)\]\((http(s?):\/\/[^\)]+)\)(#{trail})/i) do |m| | ||
| "#{$1}<a href=\"" << self.h($3) << "\" rel=\"nofollow\">" << | ||
| self.h($2) << "</a>#{$5}" | ||
| end | ||
| # find bare links that are not inside tags | ||
| # http://blah -> <a href=...> | ||
| chunk = "" | ||
| intag = false | ||
| outline = "" | ||
| line.each_byte do |n| | ||
| c = n.chr | ||
| if intag | ||
| outline << c | ||
| if c == ">" | ||
| intag = false | ||
| next | ||
| end | ||
| else | ||
| if c == "<" | ||
| if chunk != "" | ||
| outline << Markdowner._linkify_text(chunk) | ||
| end | ||
| chunk = "" | ||
| intag = true | ||
| outline << c | ||
| else | ||
| chunk << c | ||
| end | ||
| end | ||
| end | ||
| if chunk != "" | ||
| outline << Markdowner._linkify_text(chunk) | ||
| end | ||
| out << outline << "<br>\n" | ||
| end | ||
| if inpre | ||
| out << "</pre>" | ||
| end | ||
| out << "</p>" | ||
| # multiple br's into a p | ||
| out.gsub!(/<br>\n?<br>\n?/, "</p><p>") | ||
| # collapse things | ||
| out.gsub!(/<br>\n?<\/p>/, "</p>\n") | ||
| out.gsub!(/<p>\n?<br>\n?/, "<p>") | ||
| out.gsub!(/<p>\n?<\/p>/, "\n") | ||
| out.gsub!(/<p>\n?<p>/, "\n<p>") | ||
| out.gsub!(/<\/p><p>/, "</p>\n<p>") | ||
| out.strip | ||
| end | ||
| def self._linkify_text(chunk) | ||
| chunk.gsub(/ | ||
| (\A|\s|[:,]) | ||
| (http(s?):\/\/|www\.) | ||
| ([^\s]+)/ix) do |m| | ||
| pre = $1 | ||
| host_and_path = "#{$2 == "www." ? $2 : ""}#{$4}" | ||
| post = $5 | ||
| # remove some chars that might be with a url at the end but aren't | ||
| # actually part of the url | ||
| if m = host_and_path.match(/(.*)([,\?;\!\.]+)\z/) | ||
| host_and_path = m[1] | ||
| post = "#{m[2]}#{post}" | ||
| end | ||
| url = "http#{$3}://#{host_and_path}" | ||
| url_text = host_and_path | ||
| if url_text.length > 50 | ||
| url_text = url_text[0 ... 50] << "..." | ||
| end | ||
| "#{pre}<a href=\"#{url}\" rel=\"nofollow\">#{url_text}</a>#{post}" | ||
| end | ||
| end | ||
| end |