commit 093747b7961de77fefb9b4409825ad05d3275987
Author: joshua stein
Date: Sat Jun 16 20:15:46 2012 -0500
initial work on conversion from php tree
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb3489a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+#
+# If you find yourself ignoring temporary files generated by your text editor
+# or operating system, you probably want to add a global ignore instead:
+# git config --global core.excludesfile ~/.gitignore_global
+
+# Ignore bundler config
+/.bundle
+
+# Ignore the default SQLite database.
+/db/*.sqlite3
+
+# Ignore all logfiles and tempfiles.
+/log/*.log
+/tmp
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..38dcdb5
--- /dev/null
+++ b/.rspec
@@ -0,0 +1 @@
+--no-colour
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..9b578a7
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,33 @@
+source 'https://rubygems.org'
+
+gem 'rails', '3.2.2'
+
+# Bundle edge Rails instead:
+# gem 'rails', :git => 'git://github.com/rails/rails.git'
+
+gem "mysql2"
+
+gem 'jquery-rails'
+
+# To use ActiveModel has_secure_password
+gem 'bcrypt-ruby', '~> 3.0.0'
+
+gem "dynamic_form"
+
+group :test, :development do
+ gem "rspec-rails", "~> 2.6"
+ gem "machinist"
+ gem "sqlite3"
+end
+
+# To use Jbuilder templates for JSON
+# gem 'jbuilder'
+
+# Use unicorn as the app server
+# gem 'unicorn'
+
+# Deploy with Capistrano
+# gem 'capistrano'
+
+# To use debugger
+# gem 'ruby-debug19', :require => 'ruby-debug'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..86ba7e0
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,114 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actionmailer (3.2.2)
+ actionpack (= 3.2.2)
+ mail (~> 2.4.0)
+ actionpack (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ builder (~> 3.0.0)
+ erubis (~> 2.7.0)
+ journey (~> 1.0.1)
+ rack (~> 1.4.0)
+ rack-cache (~> 1.1)
+ rack-test (~> 0.6.1)
+ sprockets (~> 2.1.2)
+ activemodel (3.2.2)
+ activesupport (= 3.2.2)
+ builder (~> 3.0.0)
+ activerecord (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ arel (~> 3.0.2)
+ tzinfo (~> 0.3.29)
+ activeresource (3.2.2)
+ activemodel (= 3.2.2)
+ activesupport (= 3.2.2)
+ activesupport (3.2.2)
+ i18n (~> 0.6)
+ multi_json (~> 1.0)
+ arel (3.0.2)
+ bcrypt-ruby (3.0.1)
+ builder (3.0.0)
+ diff-lcs (1.1.3)
+ dynamic_form (1.1.4)
+ erubis (2.7.0)
+ hike (1.2.1)
+ i18n (0.6.0)
+ journey (1.0.3)
+ jquery-rails (2.0.1)
+ railties (>= 3.2.0, < 5.0)
+ thor (~> 0.14)
+ json (1.6.5)
+ machinist (2.0)
+ mail (2.4.4)
+ i18n (>= 0.4.0)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.18)
+ multi_json (1.1.0)
+ mysql2 (0.3.11)
+ polyglot (0.3.3)
+ rack (1.4.1)
+ rack-cache (1.2)
+ rack (>= 0.4)
+ rack-ssl (1.3.2)
+ rack
+ rack-test (0.6.1)
+ rack (>= 1.0)
+ rails (3.2.2)
+ actionmailer (= 3.2.2)
+ actionpack (= 3.2.2)
+ activerecord (= 3.2.2)
+ activeresource (= 3.2.2)
+ activesupport (= 3.2.2)
+ bundler (~> 1.0)
+ railties (= 3.2.2)
+ railties (3.2.2)
+ actionpack (= 3.2.2)
+ activesupport (= 3.2.2)
+ rack-ssl (~> 1.3.2)
+ rake (>= 0.8.7)
+ rdoc (~> 3.4)
+ thor (~> 0.14.6)
+ rake (0.9.2.2)
+ rdoc (3.12)
+ json (~> 1.4)
+ rspec (2.9.0)
+ rspec-core (~> 2.9.0)
+ rspec-expectations (~> 2.9.0)
+ rspec-mocks (~> 2.9.0)
+ rspec-core (2.9.0)
+ rspec-expectations (2.9.0)
+ diff-lcs (~> 1.1.3)
+ rspec-mocks (2.9.0)
+ rspec-rails (2.9.0)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ railties (>= 3.0)
+ rspec (~> 2.9.0)
+ sprockets (2.1.2)
+ hike (~> 1.2)
+ rack (~> 1.0)
+ tilt (~> 1.1, != 1.3.0)
+ sqlite3 (1.3.6)
+ thor (0.14.6)
+ tilt (1.3.3)
+ treetop (1.4.10)
+ polyglot
+ polyglot (>= 0.3.1)
+ tzinfo (0.3.32)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bcrypt-ruby (~> 3.0.0)
+ dynamic_form
+ jquery-rails
+ machinist
+ mysql2
+ rails (= 3.2.2)
+ rspec-rails (~> 2.6)
+ sqlite3
diff --git a/README.rdoc b/README.rdoc
new file mode 100644
index 0000000..7c36f23
--- /dev/null
+++ b/README.rdoc
@@ -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:
+ rails new myapp (where myapp is the application name)
+
+2. Change directory to myapp and start the web server:
+ cd myapp; rails server (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 sudo gem install ruby-debug. 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
+ => "[#nil, "body"=>nil, "id"=>"1"}>,
+ #"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
+ => #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 rails console from the application
+directory.
+
+Options:
+
+* Passing the -s, --sandbox argument will rollback any modifications
+ made to the database.
+* Passing an environment name as an argument will load the corresponding
+ environment. Example: rails console production.
+
+To reload your controllers and models after launching the console run
+reload!
+
+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 rails
+dbconsole. 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 rails dbconsole production. 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 layout :default 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 rake doc:app
+
+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.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..6408ab5
--- /dev/null
+++ b/Rakefile
@@ -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
diff --git a/app/assets/images/l.png b/app/assets/images/l.png
new file mode 100644
index 0000000..514a4d0
Binary files /dev/null and b/app/assets/images/l.png differ
diff --git a/app/assets/images/rails.png b/app/assets/images/rails.png
new file mode 100644
index 0000000..d5edc04
Binary files /dev/null and b/app/assets/images/rails.png differ
diff --git a/app/assets/images/select2.png b/app/assets/images/select2.png
new file mode 100644
index 0000000..d08e4b7
Binary files /dev/null and b/app/assets/images/select2.png differ
diff --git a/app/assets/images/superblock.png b/app/assets/images/superblock.png
new file mode 100644
index 0000000..3f25bc1
Binary files /dev/null and b/app/assets/images/superblock.png differ
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 0000000..19aa279
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -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 = $("");
+
+ var reasons;
+ if ($(el).attr("id").match(/comment/))
+ reasons = Lobsters.commentDownvoteReasons;
+ else
+ reasons = Lobsters.storyDownvoteReasons;
+
+ $.each(reasons, function(k, v) {
+ var a = $("" + v + "");
+
+ 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();
+}); */
diff --git a/app/assets/javascripts/jquery_class.js b/app/assets/javascripts/jquery_class.js
new file mode 100644
index 0000000..93e1aec
--- /dev/null
+++ b/app/assets/javascripts/jquery_class.js
@@ -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;
+ };
+})();
diff --git a/app/assets/javascripts/select2.js b/app/assets/javascripts/select2.js
new file mode 100644
index 0000000..e2a8db5
--- /dev/null
+++ b/app/assets/javascripts/select2.js
@@ -0,0 +1,1578 @@
+/*
+ Copyright 2012 Igor Vaynberg
+
+ Version: @@ver@@ Timestamp: @@timestamp@@
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in
+ compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the License is
+ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and limitations under the License.
+ */
+(function ($, undefined) {
+ "use strict";
+ /*global document, window, jQuery, console */
+
+ if (window.Select2 !== undefined) {
+ return;
+ }
+
+ var KEY, AbstractSelect2, SingleSelect2, MultiSelect2;
+
+ KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ isArrow: function (k) {
+ k = k.which ? k.which : k;
+ switch (k) {
+ case KEY.LEFT:
+ case KEY.RIGHT:
+ case KEY.UP:
+ case KEY.DOWN:
+ return true;
+ }
+ return false;
+ },
+ isControl: function (k) {
+ k = k.which ? k.which : k;
+ switch (k) {
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ }
+ };
+
+ function indexOf(value, array) {
+ var i = 0, l = array.length, v;
+
+ if (typeof value == 'undefined') {
+ return -1;
+ }
+
+ if (value.constructor === String) {
+ for (; i < l; i = i + 1) if (value.localeCompare(array[i]) === 0) return i;
+ } else {
+ for (; i < l; i = i + 1) {
+ v = array[i];
+ if (v.constructor === String) {
+ if (v.localeCompare(value) === 0) return i;
+ } else {
+ if (v === value) return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Compares equality of a and b taking into account that a and b may be strings, in which case localCompare is used
+ * @param a
+ * @param b
+ */
+ function equal(a, b) {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ if (a.constructor === String) return a.localeCompare(b) === 0;
+ if (b.constructor === String) return b.localeCompare(a) === 0;
+ return false;
+ }
+
+ /**
+ * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
+ * strings
+ * @param string
+ * @param separator
+ */
+ function splitVal(string, separator) {
+ var val, i, l;
+ if (string === null || string.length < 1) return [];
+ val = string.split(separator);
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
+ return val;
+ }
+
+ function getSideBorderPadding(element) {
+ return element.outerWidth() - element.width();
+ }
+
+ function installKeyUpChangeEvent(element) {
+ element.bind("keydown", function () {
+ element.data("keyup-change-value", element.val());
+ });
+ element.bind("keyup", function () {
+ if (element.val() !== element.data("keyup-change-value")) {
+ element.trigger("keyup-change");
+ }
+ });
+ }
+
+ /**
+ * filters mouse events so an event is fired only if the mouse moved.
+ *
+ * filters out mouse events that occur when mouse is stationary but
+ * the elements under the pointer are scrolled.
+ */
+ $(document).delegate("*", "mousemove", function (e) {
+ $(document).data("select2-lastpos", {x: e.pageX, y: e.pageY});
+ });
+ function installFilteredMouseMove(element) {
+ element.bind("mousemove", function (e) {
+ var lastpos = $(document).data("select2-lastpos");
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
+ $(e.target).trigger("mousemove-filtered", e);
+ }
+ });
+ }
+
+ /**
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
+ * within the last quietMillis milliseconds.
+ *
+ * @param quietMillis number of milliseconds to wait before invoking fn
+ * @param fn function to be debounced
+ * @return debounced version of fn
+ */
+ function debounce(quietMillis, fn) {
+ var timeout;
+ return function () {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(fn, quietMillis);
+ };
+ }
+
+ function installDebouncedScroll(threshold, element) {
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
+ element.bind("scroll", function (e) {
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
+ });
+ }
+
+ function killEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ function measureTextWidth(e) {
+ var sizer, width;
+ sizer = $("").css({
+ position: "absolute",
+ left: "-1000px",
+ top: "-1000px",
+ display: "none",
+ fontSize: e.css("fontSize"),
+ fontFamily: e.css("fontFamily"),
+ fontStyle: e.css("fontStyle"),
+ fontWeight: e.css("fontWeight"),
+ letterSpacing: e.css("letterSpacing"),
+ textTransform: e.css("textTransform"),
+ whiteSpace: "nowrap"
+ });
+ sizer.text(e.val());
+ $("body").append(sizer);
+ width = sizer.width();
+ sizer.remove();
+ return width;
+ }
+
+ /**
+ * Produces an ajax-based query function
+ *
+ * @param options object containing configuration paramters
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
+ * @param options.url url for the data
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
+ * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
+ * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
+ * The expected format is an object containing the following keys:
+ * results array of objects that will be used as choices
+ * more (optional) boolean indicating whether there are more results available
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
+ */
+ function ajax(options) {
+ var timeout, // current scheduled but not yet executed request
+ requestSequence = 0, // sequence used to drop out-of-order responses
+ handler = null,
+ quietMillis = options.quietMillis || 100;
+
+ return function (query) {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function () {
+ requestSequence += 1; // increment the sequence
+ var requestNumber = requestSequence, // this request's sequence number
+ data = options.data, // ajax data function
+ transport = options.transport || $.ajax;
+
+ data = data.call(this, query.term, query.page, query.context);
+
+ if( null !== handler){
+ handler.abort();
+ }
+ handler = transport.call(null, {
+ url: options.url,
+ dataType: options.dataType,
+ data: data,
+ success: function (data) {
+ if (requestNumber < requestSequence) {
+ return;
+ }
+ // TODO 3.0 - replace query.page with query so users have access to term, page, etc.
+ var results = options.results(data, query.page);
+ self.context = results.context;
+ query.callback(results);
+ }
+ });
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * Produces a query function that works with a local array
+ *
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
+ * object.
+ *
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
+ *
+ * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
+ * the text.
+ */
+ function local(options) {
+ var data = options, // data elements
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
+
+ if (!$.isArray(data)) {
+ text = data.text;
+ // if text is not a function we assume it to be a key name
+ if (!$.isFunction(text)) text = function (item) { return item[data.text]; };
+ data = data.results;
+ }
+
+ return function (query) {
+ var t = query.term, filtered = {};
+ if (t === "") {
+ query.callback({results: data});
+ return;
+ }
+ filtered.results = $(data)
+ .filter(function () {return query.matcher(t, text(this));})
+ .get();
+ query.callback(filtered);
+ };
+ }
+
+ // TODO javadoc
+ function tags(data) {
+ // TODO even for a function we should probably return a wrapper that does the same object/string check as
+ // the function for arrays. otherwise only functions that return objects are supported.
+ if ($.isFunction(data)) {
+ return data;
+ }
+
+ // if not a function we assume it to be an array
+
+ return function (query) {
+ var t = query.term, filtered = {results: []};
+ $(data).each(function () {
+ var isObject = this.text !== undefined,
+ text = isObject ? this.text : this;
+ if (t === "" || query.matcher(t, text)) {
+ filtered.results.push(isObject ? this : {id: this, text: this});
+ }
+ });
+ query.callback(filtered);
+ };
+ }
+
+ /**
+ * blurs any Select2 container that has focus when an element outside them was clicked or received focus
+ */
+ $(document).ready(function () {
+ $(document).delegate("*", "mousedown focusin touchend", function (e) {
+ var target = $(e.target).closest("div.select2-container").get(0);
+ $(document).find("div.select2-container-active").each(function () {
+ if (this !== target) $(this).data("select2").blur();
+ });
+ });
+ });
+
+ /**
+ * Creates a new class
+ *
+ * @param superClass
+ * @param methods
+ */
+ function clazz(SuperClass, methods) {
+ var constructor = function () {};
+ constructor.prototype = new SuperClass;
+ constructor.prototype.constructor = constructor;
+ constructor.prototype.parent = SuperClass.prototype;
+ constructor.prototype = $.extend(constructor.prototype, methods);
+ return constructor;
+ }
+
+ AbstractSelect2 = clazz(Object, {
+
+ bind: function (func) {
+ var self = this;
+ return function () {
+ func.apply(self, arguments);
+ };
+ },
+
+ init: function (opts) {
+ var results, search, resultsSelector = ".select2-results";
+
+ // prepare options
+ this.opts = opts = this.prepareOpts(opts);
+
+ this.id=opts.id;
+
+ // destroy if called on an existing component
+ if (opts.element.data("select2") !== undefined &&
+ opts.element.data("select2") !== null) {
+ this.destroy();
+ }
+
+ this.enabled=true;
+ this.container = this.createContainer();
+
+ if (opts.element.attr("class") !== undefined) {
+ this.container.addClass(opts.element.attr("class"));
+ }
+
+ // swap container for the element
+ this.opts.element
+ .data("select2", this)
+ .hide()
+ .after(this.container);
+ this.container.data("select2", this);
+
+ this.dropdown = this.container.find(".select2-drop");
+ this.results = results = this.container.find(resultsSelector);
+ this.search = search = this.container.find("input[type=text]");
+
+ this.resultsPage = 0;
+ this.context = null;
+
+ // initialize the container
+ this.initContainer();
+
+ installFilteredMouseMove(this.results);
+ this.container.delegate(resultsSelector, "mousemove-filtered", this.bind(this.highlightUnderEvent));
+
+ installDebouncedScroll(80, this.results);
+ this.container.delegate(resultsSelector, "scroll-debounced", this.bind(this.loadMoreIfNeeded));
+
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
+ if ($.fn.mousewheel) {
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
+ var top = results.scrollTop(), height;
+ if (deltaY > 0 && top - deltaY <= 0) {
+ results.scrollTop(0);
+ killEvent(e);
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
+ results.scrollTop(results.get(0).scrollHeight - results.height());
+ killEvent(e);
+ }
+ });
+ }
+
+ installKeyUpChangeEvent(search);
+ search.bind("keyup-change", this.bind(this.updateResults));
+ search.bind("focus", function () { search.addClass("select2-focused");});
+ search.bind("blur", function () { search.removeClass("select2-focused");});
+
+ this.container.delegate(resultsSelector, "click", this.bind(function (e) {
+ if ($(e.target).closest(".select2-result:not(.select2-disabled)").length > 0) {
+ this.highlightUnderEvent(e);
+ this.selectHighlighted(e);
+ } else {
+ killEvent(e);
+ this.focusSearch();
+ }
+ }));
+
+ if ($.isFunction(this.opts.initSelection)) {
+ // initialize selection based on the current value of the source element
+ this.initSelection();
+
+ // if the user has provided a function that can set selection based on the value of the source element
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
+ this.monitorSource();
+ }
+
+ if (opts.element.is(":disabled")) this.disable();
+ },
+
+ destroy: function () {
+ var select2 = this.opts.element.data("select2");
+ if (select2 !== undefined) {
+ select2.container.remove();
+ select2.opts.element
+ .removeData("select2")
+ .unbind(".select2")
+ .show();
+ }
+ },
+
+ prepareOpts: function (opts) {
+ var element, select, idKey;
+
+ element = opts.element;
+
+ if (element.get(0).tagName.toLowerCase() === "select") {
+ this.select = select = opts.element;
+ }
+
+ if (select) {
+ // these options are not allowed when attached to a select because they are picked up off the element itself
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
+ if (this in opts) {
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a
diff --git a/app/views/users/show.phtml b/app/views/users/show.phtml
new file mode 100644
index 0000000..3154716
--- /dev/null
+++ b/app/views/users/show.phtml
@@ -0,0 +1,24 @@
+
= h($showing_user->username); ?>
+
+ a user for = $time->time_ago_in_words($showing_user->created_at); ?>
+
+
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..2d27577
--- /dev/null
+++ b/config.ru
@@ -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
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..a00b355
--- /dev/null
+++ b/config/application.rb
@@ -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
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..4489e58
--- /dev/null
+++ b/config/boot.rb
@@ -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'])
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..975c02b
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the rails application
+require File.expand_path('../application', __FILE__)
+
+# Initialize the rails application
+Lobsters::Application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..430e967
--- /dev/null
+++ b/config/environments/development.rb
@@ -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
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..7bf51ac
--- /dev/null
+++ b/config/environments/production.rb
@@ -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
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..05105ca
--- /dev/null
+++ b/config/environments/test.rb
@@ -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
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000..59385cd
--- /dev/null
+++ b/config/initializers/backtrace_silencers.rb
@@ -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!
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..5d8d9be
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -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
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
new file mode 100644
index 0000000..72aca7e
--- /dev/null
+++ b/config/initializers/mime_types.rb
@@ -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
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
new file mode 100644
index 0000000..adb3434
--- /dev/null
+++ b/config/initializers/session_store.rb
@@ -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
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000..999df20
--- /dev/null
+++ b/config/initializers/wrap_parameters.rb
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..179c14c
--- /dev/null
+++ b/config/locales/en.yml
@@ -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"
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..27a6d44
--- /dev/null
+++ b/config/routes.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..d5acb63
--- /dev/null
+++ b/db/schema.rb
@@ -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
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..4edb1e8
--- /dev/null
+++ b/db/seeds.rb
@@ -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)
diff --git a/extras/markdowner.rb b/extras/markdowner.rb
new file mode 100644
index 0000000..2cd7a58
--- /dev/null
+++ b/extras/markdowner.rb
@@ -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 = "
")
+
+ 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}#{url_text}#{post}"
+ end
+ end
+end
diff --git a/extras/sponge.rb b/extras/sponge.rb
new file mode 100644
index 0000000..0b890a5
--- /dev/null
+++ b/extras/sponge.rb
@@ -0,0 +1,217 @@
+require "uri"
+require "net/https"
+require "resolv"
+require "ipaddr"
+
+class Sponge
+ MAX_TIME = 60
+ MAX_DNS_TIME = 5
+
+ attr_accessor :debug, :last_res, :timeout
+
+ # rfc3330
+ BAD_NETS = [
+ "0.0.0.0/8",
+ "10.0.0.0/8",
+ "127.0.0.0/8",
+ "169.254.0.0/16",
+ "172.16.0.0/12",
+ "192.0.2.0/24",
+ "192.88.99.0/24",
+ "192.168.0.0/16",
+ "198.18.0.0/15",
+ "224.0.0.0/4",
+ "240.0.0.0/4"
+ ]
+
+ # old api
+ def self.fetch(url, headers = {}, limit = 10)
+ s = Sponge.new
+ s.fetch(url, "get", nil, nil, headers, limit)
+ end
+
+ def initialize
+ @cookies = {}
+ @timeout = MAX_TIME
+ end
+
+ def set_cookie(host, name, val)
+ dputs "setting cookie #{name} on domain #{host} to #{val}"
+
+ if !@cookies[host]
+ @cookies[host] = {}
+ end
+
+ if val.to_s == ""
+ @cookies[host][name] ? @cookies[host][name].delete : nil
+ else
+ @cookies[host][name] = val
+ end
+ end
+
+ def cookies(host)
+ cooks = @cookies[host] || {}
+
+ # check for domain cookies
+ @cookies.keys.each do |dom|
+ if dom.length < host.length &&
+ dom == host[host.length - dom.length .. host.length - 1]
+ dputs "adding domain keys from #{dom}"
+ cooks = cooks.merge @cookies[dom]
+ end
+ end
+
+ if cooks
+ return cooks.map{|k,v| "#{k}=#{v};" }.join(" ")
+ else
+ return ""
+ end
+ end
+
+ def fetch(url, method = :get, fields = nil, raw_post_data = nil,
+ headers = {}, limit = 10)
+ raise ArgumentError, "http redirection too deep" if limit <= 0
+
+ uri = URI.parse(url)
+
+ # we'll manually resolve the ip so we can verify it's not local
+ ip = nil
+ tip = nil
+ ips = []
+ retried = false
+ begin
+ Timeout.timeout(MAX_DNS_TIME) do
+ ips = Resolv.getaddresses(uri.host)
+
+ if !ips.any?
+ raise
+ end
+
+ # pick a random one
+ tip = ips[rand(ips.length)]
+ ip = IPAddr.new(tip)
+ end
+ rescue Timeout::Error => e
+ if retried
+ raise "couldn't resolve #{uri.host} (DNS timeout)"
+ else
+ retried = true
+ retry
+ end
+ rescue StandardError => e
+ raise "couldn't resolve #{uri.host} (#{e.inspect})"
+ end
+
+ if !ip
+ raise "couldn't resolve #{uri.host}"
+ end
+
+ if BAD_NETS.select{|n| IPAddr.new(n).include?(ip) }.any?
+ raise "refusing to talk to IP #{ip.to_s}"
+ end
+
+ host = Net::HTTP.new(ip.to_s, uri.port)
+ if self.debug
+ host.set_debug_output $stdout
+ end
+
+ if uri.scheme == "https"
+ host.use_ssl = true
+ host.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+
+ path = (uri.path == "" ? "/" : uri.path)
+ if uri.query
+ path += "?" + uri.query
+ elsif method == :get && raw_post_data
+ path += "?" + URI.encode(raw_post_data)
+ headers["Content-type"] = "application/x-www-form-urlencoded"
+ end
+
+ if method == :post
+ if raw_post_data
+ post_data = raw_post_data
+ headers["Content-type"] = "application/x-www-form-urlencoded"
+ else
+ post_data = fields.map{|k,v| "#{k}=#{v}" }.join("&")
+ end
+
+ headers["Content-Length"] = post_data.length.to_s
+ end
+
+ path.gsub!(/^\/\//, "/")
+
+ dputs "fetching #{url} (#{ip.to_s}) " + (uri.user ? "with http auth " +
+ uri.user + "/" + ("*" * uri.password.length) + " " : "") +
+ "by #{method} with cookies #{cookies(uri.host)}"
+
+ headers = {
+ "Host" => uri.host,
+ "Cookie" => cookies(uri.host),
+ "Referer" => url.to_s,
+ "User-Agent" => "Mozilla/5.0 (compatible)",
+ }.merge(headers || {})
+
+ if uri.user
+ headers["Authorization"] = "Basic " +
+ ["#{uri.user}:#{uri.password}"].pack('m').delete("\r\n")
+ end
+
+ res = nil
+ Timeout.timeout(self.timeout) do
+ if method == :post
+ res = host.post(path, post_data, headers)
+ else
+ res = host.get(path, headers)
+ end
+ end
+
+ if res.get_fields("Set-Cookie")
+ res.get_fields("Set-Cookie").each do |cook|
+ if p = Regexp.new(/^([^=]+)=([^;]*)/).match(cook)
+ set_cookie(uri.host, p[1], p[2])
+ else
+ dputs "unable to match cookie line #{cook}"
+ end
+ end
+ end
+
+ last_res = res
+
+ case res
+ when Net::HTTPSuccess
+ return res.body
+ when Net::HTTPRedirection
+ # follow
+ newuri = URI.parse(res["location"])
+ if newuri.host
+ dputs "following redirection to " + res["location"]
+ else
+ # relative path
+ newuri.host = uri.host
+ newuri.scheme = uri.scheme
+ newuri.port = uri.port
+ newuri.path = "/#{newuri.path}"
+
+ dputs "following relative redirection to " + newuri.to_s
+ end
+
+ fetch(newuri.to_s, "get", nil, nil, headers, limit - 1)
+ end
+ end
+
+ def get(url)
+ fetch(url, "get")
+ end
+
+ def post(url, fields)
+ fetch(url, "post", fields)
+ end
+
+private
+ def dputs(string)
+ if self.debug
+ puts string
+ end
+ end
+end
diff --git a/extras/utils.rb b/extras/utils.rb
new file mode 100644
index 0000000..feb7fb4
--- /dev/null
+++ b/extras/utils.rb
@@ -0,0 +1,16 @@
+class Utils
+ def self.random_str(len)
+ str = ""
+ while str.length < len
+ chr = OpenSSL::Random.random_bytes(1)
+ ord = chr.unpack('C')[0]
+
+ # 0 9 A Z a z
+ if (ord >= 48 && ord <= 57) || (ord >= 65 && ord <= 90) || (ord >= 97 && ord <= 122)
+ str += chr
+ end
+ end
+
+ return str
+ end
+end
diff --git a/log/.gitkeep b/log/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..9a48320
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,26 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..83660ab
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,26 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..f3648a0
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,25 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e80e606
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..085187f
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-Agent: *
+# Disallow: /
diff --git a/script/rails b/script/rails
new file mode 100755
index 0000000..f8da2cf
--- /dev/null
+++ b/script/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require File.expand_path('../../config/boot', __FILE__)
+require 'rails/commands'
diff --git a/spec/models/markdowner_spec.rb b/spec/models/markdowner_spec.rb
new file mode 100644
index 0000000..efdede9
--- /dev/null
+++ b/spec/models/markdowner_spec.rb
@@ -0,0 +1,104 @@
+require "spec_helper"
+
+def m(inp, out)
+ Markdowner::markdown(inp).should == out
+end
+
+describe Markdowner do
+ it "converts indented text into
" do
+ m " This is some\n text.\n",
+ "
This is some\n text.\n
"
+
+ m " blah ",
+ "
blah <script>alert('hi');</script>\n
"
+ end
+
+ it "converts text surrounded by * to " do
+ m "oh hullo *there*",
+ "
oh hullo there
"
+
+ m "*hi*",
+ "
hi
"
+
+ m "* hi hello*zap zap*",
+ "
* hi hello*zap zap*
"
+
+ m "oh hullo * there*",
+ "
oh hullo * there*
"
+
+ m " oh hullo *there*",
+ "
oh hullo *there*\n
"
+
+ m "oh hullo*there*",
+ "
oh hullo*there*
"
+ end
+
+ it "converts text surrounded by _ to " do
+ m "oh hullo _there_",
+ "
oh hullo there
"
+
+ m "oh hullo _ there_",
+ "
oh hullo _ there_
"
+
+ m "oh hullo _there_ and *yes* i see",
+ "
oh hullo there and yes i see
"
+ end
+
+ it "combines conversions" do
+ m "oh _*hullo*_ there_",
+ "
oh hullo there_
"
+
+ m "oh *_hullo_* there_",
+ "
oh hullo there_
"
+
+ m "oh *[hello](http://jcs.org/)* there_",
+ "
"
+ end
+
+ it "truncates long url titles" do
+ m "a long http://www.example.com/goes/here/and/this/is/a/long/" <<
+ "url/which/should.get.shortened.html?because=this+will+cause+" <<
+ "the+page+to+wrap&such+ok",
+ "
"
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..6953e45
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,32 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+ENV["RAILS_ENV"] ||= 'test'
+require File.expand_path("../../config/environment", __FILE__)
+require 'rspec/rails'
+require 'rspec/autorun'
+
+# Requires supporting ruby files with custom matchers and macros, etc,
+# in spec/support/ and its subdirectories.
+Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
+
+RSpec.configure do |config|
+ # ## Mock Framework
+ #
+ # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
+ #
+ # config.mock_with :mocha
+ # config.mock_with :flexmock
+ # config.mock_with :rr
+
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ #config.fixture_path = "#{::Rails.root}/spec/fixtures"
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # If true, the base class of anonymous controllers will be inferred
+ # automatically. This will be the default behavior in future versions of
+ # rspec-rails.
+ config.infer_base_class_for_anonymous_controllers = false
+end
diff --git a/spec/support/blueprints.rb b/spec/support/blueprints.rb
new file mode 100644
index 0000000..2c5efe3
--- /dev/null
+++ b/spec/support/blueprints.rb
@@ -0,0 +1,7 @@
+require 'machinist/active_record'
+
+User.blueprint do
+ email { "user-#{sn}@example.com" }
+ password { "blah blah" }
+ password_confirmation { object.password }
+end
diff --git a/vendor/assets/javascripts/.gitkeep b/vendor/assets/javascripts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/assets/stylesheets/.gitkeep b/vendor/assets/stylesheets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/plugins/.gitkeep b/vendor/plugins/.gitkeep
new file mode 100644
index 0000000..e69de29