initial work on conversion from php tree
This commit is contained in:
commit
093747b796
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
@ -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
|
33
Gemfile
Normal file
33
Gemfile
Normal file
|
@ -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'
|
114
Gemfile.lock
Normal file
114
Gemfile.lock
Normal file
|
@ -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
|
261
README.rdoc
Normal file
261
README.rdoc
Normal file
|
@ -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.
|
7
Rakefile
Normal file
7
Rakefile
Normal file
|
@ -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
|
BIN
app/assets/images/l.png
Normal file
BIN
app/assets/images/l.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 268 B |
BIN
app/assets/images/rails.png
Normal file
BIN
app/assets/images/rails.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
app/assets/images/select2.png
Normal file
BIN
app/assets/images/select2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 396 B |
BIN
app/assets/images/superblock.png
Normal file
BIN
app/assets/images/superblock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 213 B |
149
app/assets/javascripts/application.js
Normal file
149
app/assets/javascripts/application.js
Normal file
|
@ -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();
|
||||
}); */
|
64
app/assets/javascripts/jquery_class.js
vendored
Normal file
64
app/assets/javascripts/jquery_class.js
vendored
Normal file
|
@ -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;
|
||||
};
|
||||
})();
|
1578
app/assets/javascripts/select2.js
Normal file
1578
app/assets/javascripts/select2.js
Normal file
File diff suppressed because it is too large
Load diff
866
app/assets/stylesheets/application.css
Normal file
866
app/assets/stylesheets/application.css
Normal file
|
@ -0,0 +1,866 @@
|
|||
/*
|
||||
*= require_tree .
|
||||
*= require_self
|
||||
*/
|
||||
|
||||
|
||||
/* generics */
|
||||
|
||||
/* force a vertical scrollbar on firefox to avoid page-shifting */
|
||||
html {
|
||||
overflow: -moz-scrollbars-vertical !important;
|
||||
}
|
||||
|
||||
body, textarea, input {
|
||||
font-family: "helvetica neue", arial, sans-serif;
|
||||
font-size: 10pt;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0855f1;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #4342AC;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
margin-bottom: 0.75em;
|
||||
padding: 0px;
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div.s {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
a.tag {
|
||||
border: 1px solid #b8b599;
|
||||
background-color: #fffcd7;
|
||||
font-size: 7.5pt;
|
||||
margin-left: 0.25em;
|
||||
padding: 0px 0.5em 1px 0.5em;
|
||||
text-decoration: none;
|
||||
-moz-border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
color: #555;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
|
||||
/* default form styling */
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
color: #555;
|
||||
line-height: 1.2em;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
textarea {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
select {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border-color: #aaa;
|
||||
color: #303030;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
body.mobile input,
|
||||
body.mobile select,
|
||||
body.mobile textarea {
|
||||
max-width: 275px;
|
||||
}
|
||||
|
||||
/* these must be separate */
|
||||
::-webkit-input-placeholder {
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
:-moz-placeholder {
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
div.select2-choices {
|
||||
display: inline-block;
|
||||
padding: 2px 10px 2px 10px;
|
||||
line-height: 18px;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-color: #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:first-child,
|
||||
input[type="button"]:first-child,
|
||||
input[type="reset"]:first-child,
|
||||
input[type="submit"]:first-child {
|
||||
*margin-left: 0;
|
||||
}
|
||||
button:hover,
|
||||
input[type="button"]:hover,
|
||||
input[type="reset"]:hover,
|
||||
input[type="submit"]:hover {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
select {
|
||||
margin-top: 3px;
|
||||
min-width: 100px;
|
||||
}
|
||||
select:focus {
|
||||
border-color: rgba(160,160,160,.8);
|
||||
color: #303030;
|
||||
outline: 0;
|
||||
}
|
||||
select::-moz-focus-inner {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
div.field_with_errors {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.field_with_errors input[type="text"],
|
||||
div.field_with_errors input[type="email"],
|
||||
div.field_with_errors input[type="password"] {
|
||||
border: 1px solid red;
|
||||
}
|
||||
div.field_with_errors input:focus {
|
||||
border-color: rgba(255,0,0,.8);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background-color: #e9e9e9;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
/* outliners */
|
||||
|
||||
div#wrapper {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: 1em;
|
||||
width: 900px;
|
||||
}
|
||||
div#inside {
|
||||
margin-left: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
|
||||
/* header */
|
||||
|
||||
#header {
|
||||
font-weight: bold;
|
||||
padding: 5px 42px 5px 10px;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
|
||||
#l_holder {
|
||||
background-color: #ee0000;
|
||||
float: left;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 1px;
|
||||
margin-left: 4px;
|
||||
margin-right: 11px;
|
||||
}
|
||||
#l_holder.dead {
|
||||
background-color: black;
|
||||
}
|
||||
#l_holder.boring {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-size: 11pt;
|
||||
margin: 0px;
|
||||
margin-left: 22px;
|
||||
margin-right: 1em;
|
||||
padding: 0px;
|
||||
display: inline;
|
||||
}
|
||||
#header h1 a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#headertitle {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
#headertitle a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#headerlinks a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
#headerright {
|
||||
float: right;
|
||||
color: #888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#headerright a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#headerright.loggedin a {
|
||||
padding-left: 0.75em;
|
||||
}
|
||||
|
||||
|
||||
/* footer */
|
||||
|
||||
div#footer {
|
||||
clear: both;
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
margin-bottom: 2em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
div#footer a {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
/* stories */
|
||||
|
||||
ol.stories,
|
||||
ol.comments {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ol.comments {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
div.voters {
|
||||
float: left;
|
||||
margin-top: -5px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
div.voters div.score {
|
||||
color: #aaa;
|
||||
font-size: 9pt;
|
||||
margin-top: 1px;
|
||||
margin-bottom: -2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.voters a.upvoter,
|
||||
div.voters a.downvoter {
|
||||
border-color: transparent transparent #bbb transparent;
|
||||
border-style: solid;
|
||||
border-width: 6px;
|
||||
text-decoration: none;
|
||||
width: 0px;
|
||||
height: 0;
|
||||
margin-bottom: 0px;
|
||||
margin-left: 14px;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.voters a.upvoter:hover,
|
||||
li.upvoted div.voters a.upvoter {
|
||||
border-bottom-color: #ac130d;
|
||||
}
|
||||
|
||||
div.voters a.upvoter {
|
||||
border-bottom-width: 11px;
|
||||
}
|
||||
|
||||
div.voters a.downvoter {
|
||||
border-color: #bbb transparent transparent transparent;
|
||||
border-width: 5px;
|
||||
margin-top: 4px;
|
||||
margin-left: 15px;
|
||||
margin-bottom: -5px;
|
||||
border-top-width: 9px;
|
||||
}
|
||||
div.voters a.downvoter:hover,
|
||||
li.downvoted div.voters a.downvoter {
|
||||
border-top-color: gray;
|
||||
}
|
||||
|
||||
ol.stories li,
|
||||
ol.comments li {
|
||||
clear: both;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
li div.details {
|
||||
padding-top: 0.1em;
|
||||
}
|
||||
ol.stories li.downvoted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
ol.comments li.negative {
|
||||
opacity: 0.7;
|
||||
color: gray !important;
|
||||
}
|
||||
ol.comments li.negative_3 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
ol.comments li.negative_5 {
|
||||
opacity: 0.3;
|
||||
}
|
||||
ol.comments li.negative_7 {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
li .link {
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
li .link a {
|
||||
font-size: 12pt;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ol.stories a.tag {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
li .domain {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
font-size: 8.5pt;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
li .byline {
|
||||
color: #888;
|
||||
font-size: 8.5pt;
|
||||
}
|
||||
ol.stories li .byline {
|
||||
margin-top: 1px;
|
||||
}
|
||||
ol.comments li .byline {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
li .byline a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
li.story.expired {
|
||||
opacity: 0.6;
|
||||
}
|
||||
li.story.expired a {
|
||||
color: gray !important;
|
||||
}
|
||||
|
||||
li div.details,
|
||||
div.story_content {
|
||||
margin-left: 40px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
div.story_text {
|
||||
font-size: 10pt;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.story_text p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
blockquote {
|
||||
display: inline;
|
||||
font-style: italic;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 0.5em;
|
||||
border-left: 2px solid gray;
|
||||
}
|
||||
|
||||
div.comment_text p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
div.comment_actions a {
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-size: 8.5pt;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
a.pagelink {
|
||||
border: 1px solid #ddd;
|
||||
background-color: #fbfbfb;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
a.pagelink.curpage {
|
||||
border: 1px solid gray;
|
||||
background-color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
#downvote_why {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
border: 1px solid #aaa;
|
||||
border-bottom: 0;
|
||||
}
|
||||
#downvote_why a {
|
||||
background-color: white;
|
||||
color: #555;
|
||||
border-bottom: 1px solid #aaa;
|
||||
display: block;
|
||||
padding: 3px;
|
||||
font-size: 9pt;
|
||||
}
|
||||
#downvote_why a:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
/* data tables */
|
||||
|
||||
table.data caption {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
table.data .capright {
|
||||
float: right;
|
||||
}
|
||||
|
||||
table.data th {
|
||||
background-color: #eaeaea;
|
||||
border-bottom: 1px solid #cacaca;
|
||||
border-top: 1px solid #cacaca;
|
||||
padding: 2px;
|
||||
padding-left: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
table.data th img {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
table.data th.r, table.data td.r {
|
||||
text-align: right;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
table.data td {
|
||||
padding-left: 3px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
table.thread td {
|
||||
padding: 0px 0px 0px 3px;
|
||||
margin-bottom: 0px;
|
||||
line-height: 17px;
|
||||
/* text-vertical-align: middle; */
|
||||
overflow: hidden;
|
||||
}
|
||||
table.thread td img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table.data tr.row0 td {
|
||||
background-color: #f8f8f8;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
table.data tr.row1 td {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
table.data tr.nobottom td {
|
||||
border-bottom: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
table.data tr.void td, table.data tr.void td a {
|
||||
text-decoration: line-through;
|
||||
color: gray !important;
|
||||
}
|
||||
|
||||
|
||||
/* boxes */
|
||||
.box {
|
||||
border: 0;
|
||||
margin: 0 40px;
|
||||
padding: 0;
|
||||
}
|
||||
.box_submitter {
|
||||
border: 1px solid #cacaca;
|
||||
border-top: 0px;
|
||||
background-color: #eaeaea;
|
||||
padding: 1em;
|
||||
}
|
||||
.box_submitter input {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.box .legend {
|
||||
background-color: white;
|
||||
margin-bottom: 1em;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
font-size: 11pt;
|
||||
}
|
||||
.box .boxtitle {
|
||||
background-color: #f0f0f0;
|
||||
border-bottom: 1px solid #cacaca;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin: -3px -7px 4px -7px;
|
||||
padding: 3px 5px 3px 5px;
|
||||
}
|
||||
|
||||
.box label,
|
||||
.box .label_holder {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.box label.required {
|
||||
font-weight: bold;
|
||||
}
|
||||
.box img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.box label,
|
||||
.box span,
|
||||
.box select,
|
||||
.box br {
|
||||
line-height: 2em;
|
||||
}
|
||||
.box br {
|
||||
clear: both;
|
||||
}
|
||||
.box .boxline {
|
||||
clear: both;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.box p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.box textarea {
|
||||
margin-bottom: 4px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.box input.normal,
|
||||
.box label.normal {
|
||||
display: inline;
|
||||
float: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.box label,
|
||||
.box .label_holder {
|
||||
width: 7em;
|
||||
line-height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.box .d {
|
||||
margin-left: 10em;
|
||||
}
|
||||
|
||||
.box .label_holder label,
|
||||
.box .label_holder input {
|
||||
line-height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.hintblock {
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
margin-left: 7em;
|
||||
}
|
||||
|
||||
.box.wide label,
|
||||
.box.wide .label_holder {
|
||||
width: 12em;
|
||||
}
|
||||
.box.wide .hintblock {
|
||||
margin-left: 12em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* for flash_notices() and flash_errors() */
|
||||
|
||||
div.flash-error,
|
||||
div.flash-notice,
|
||||
div.flash-success,
|
||||
div.errorExplanation {
|
||||
position: relative;
|
||||
padding: 7px 15px;
|
||||
margin-bottom: 18px;
|
||||
color: white;
|
||||
background-color: #eedc94;
|
||||
background-repeat: repeat-x;
|
||||
background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));
|
||||
background-image: -moz-linear-gradient(top, #fceec1, #eedc94);
|
||||
background-image: -ms-linear-gradient(top, #fceec1, #eedc94);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));
|
||||
background-image: -webkit-linear-gradient(top, #fceec1, #eedc94);
|
||||
background-image: -o-linear-gradient(top, #fceec1, #eedc94);
|
||||
background-image: linear-gradient(top, #fceec1, #eedc94);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
border-color: #eedc94 #eedc94 #e4c652;
|
||||
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
div.flash-error a,
|
||||
div.flash-notice a,
|
||||
div.flash-success a,
|
||||
div.errorExplanation a {
|
||||
font-weight: bold;
|
||||
color: #404040;
|
||||
}
|
||||
div.flash-error p a,
|
||||
div.flash-notice p a,
|
||||
div.flash-success p a,
|
||||
div.errorExplanation a {
|
||||
color: #ffffff;
|
||||
}
|
||||
div.flash-error p,
|
||||
div.flash-notice p,
|
||||
div.flash-success p,
|
||||
div.errorExplanation p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.flash-error div,
|
||||
div.flash-notice div,
|
||||
div.flash-success div,
|
||||
div.errorExplanation div {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 2px;
|
||||
line-height: 28px;
|
||||
}
|
||||
div.flash-error,
|
||||
div.errorExplanation {
|
||||
background-color: #c43c35;
|
||||
background-repeat: repeat-x;
|
||||
background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));
|
||||
background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
|
||||
background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));
|
||||
background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
|
||||
background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
|
||||
background-image: linear-gradient(top, #ee5f5b, #c43c35);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
border-color: #c43c35 #c43c35 #882a25;
|
||||
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
div.flash-success {
|
||||
background-color: #57a957;
|
||||
background-repeat: repeat-x;
|
||||
background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));
|
||||
background-image: -moz-linear-gradient(top, #62c462, #57a957);
|
||||
background-image: -ms-linear-gradient(top, #62c462, #57a957);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));
|
||||
background-image: -webkit-linear-gradient(top, #62c462, #57a957);
|
||||
background-image: -o-linear-gradient(top, #62c462, #57a957);
|
||||
background-image: linear-gradient(top, #62c462, #57a957);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
border-color: #57a957 #57a957 #3d773d;
|
||||
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
div.flash-notice {
|
||||
background-color: #339bb9;
|
||||
background-repeat: repeat-x;
|
||||
background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));
|
||||
background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
|
||||
background-image: -ms-linear-gradient(top, #5bc0de, #339bb9);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));
|
||||
background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
|
||||
background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
|
||||
background-image: linear-gradient(top, #5bc0de, #339bb9);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
border-color: #339bb9 #339bb9 #22697d;
|
||||
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
div.errorExplanation h2 {
|
||||
font-size: 1.25em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div#flasher {
|
||||
position: fixed;
|
||||
top: -40px;
|
||||
right: 0px;
|
||||
left: 325px;
|
||||
text-align: center;
|
||||
z-index: 15;
|
||||
height: 1px;
|
||||
}
|
||||
div#flasher div.flash-error,
|
||||
div#flasher div.flash-notice,
|
||||
div#flasher div.flash-success {
|
||||
line-height: 1.5em;
|
||||
display: inline-block;
|
||||
padding-top: 25px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
div#flasher span#flashcontent {
|
||||
}
|
||||
|
||||
|
||||
/* for error_messages_for() */
|
||||
div.fieldWithErrors {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/* modal confirm() */
|
||||
div.modal_confirm {
|
||||
background-color: black;
|
||||
opacity: 0.6;
|
||||
filter: alpha(opacity=60);
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
z-index: 20;
|
||||
}
|
||||
div.modal_confirm_wrapper {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
z-index: 21;
|
||||
}
|
||||
div.modal_confirm_content {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
|
||||
padding: 15px;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100);
|
||||
text-align: center;
|
||||
margin-top: 150px;
|
||||
width: 35%;
|
||||
z-index: 22;
|
||||
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
-webkit-box-shadow: 0px 0px 10px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 0px 0px 10px rgba(0,0,0,0.75);
|
||||
box-shadow: 0px 0px 10px rgba(0,0,0,0.75);
|
||||
}
|
||||
div.modal_confirm_content div.modal_confirm_buttonbar {
|
||||
margin-top: 1em;
|
||||
}
|
||||
div.modal_confirm_content div.modal_confirm_buttonbar input.default_button {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/* select2 deuglification */
|
||||
|
||||
.select2-container-multi.select2-container-active .select2-choices {
|
||||
border: 1px solid rgba(160,160,160,.8);
|
||||
}
|
||||
.select2-container .select2-results .select2-highlighted {
|
||||
background: darkred;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices {
|
||||
border-color: #ccc;
|
||||
background-image: none;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.select2-container-multi.select2-container-active .select2-choices {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow : none;
|
||||
-o-box-shadow : none;
|
||||
box-shadow : none;
|
||||
}
|
||||
|
||||
.select2-container-active .select2-choice,
|
||||
.select2-container-active .select2-choices {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow : none;
|
||||
-o-box-shadow : none;
|
||||
box-shadow : none;
|
||||
}
|
||||
|
||||
.select2-dropdown-open .select2-choice {
|
||||
border: 1px solid #aaa;
|
||||
border-bottom-color: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow : none;
|
||||
-o-box-shadow : none;
|
||||
box-shadow : none;
|
||||
}
|
456
app/assets/stylesheets/select2.css
Normal file
456
app/assets/stylesheets/select2.css
Normal file
|
@ -0,0 +1,456 @@
|
|||
/*
|
||||
Version: @@ver@@ Timestamp: @@timestamp@@
|
||||
*/
|
||||
.select2-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
/* inline-block for ie7 */
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
|
||||
}
|
||||
|
||||
.select2-container,
|
||||
.select2-drop,
|
||||
.select2-search,
|
||||
.select2-container .select2-search input{
|
||||
/*
|
||||
Force border-box so that % widths fit the parent
|
||||
container without overlap because of margin/padding.
|
||||
|
||||
More Info : http://www.quirksmode.org/css/box.html
|
||||
*/
|
||||
-moz-box-sizing: border-box; /* firefox */
|
||||
-ms-box-sizing: border-box; /* ie */
|
||||
-webkit-box-sizing: border-box; /* webkit */
|
||||
-khtml-box-sizing: border-box; /* konqueror */
|
||||
box-sizing: border-box; /* css3 */
|
||||
}
|
||||
|
||||
.select2-container .select2-choice {
|
||||
background-color: #fff;
|
||||
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
|
||||
background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
|
||||
background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
|
||||
background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%);
|
||||
background-image: -ms-linear-gradient(top, #eeeeee 0%, #ffffff 50%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#ffffff', GradientType = 0);
|
||||
background-image: linear-gradient(top, #eeeeee 0%, #ffffff 50%);
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #aaa;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 0 0 8px;
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.select2-container .select2-choice span {
|
||||
margin-right: 26px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
-o-text-overflow: ellipsis;
|
||||
-ms-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.select2-container .select2-choice abbr {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
font-size: 1px;
|
||||
background: url(select2.png) right top no-repeat;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border:0;
|
||||
outline: 0;
|
||||
}
|
||||
.select2-container .select2-choice abbr:hover {
|
||||
background-position: right -11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select2-container .select2-drop {
|
||||
background: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-top: 0;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
||||
-moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
||||
-o-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
||||
box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
|
||||
z-index: 999;
|
||||
width:100%;
|
||||
margin-top:-1px;
|
||||
|
||||
-webkit-border-radius: 0 0 4px 4px;
|
||||
-moz-border-radius: 0 0 4px 4px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.select2-container .select2-choice div {
|
||||
-webkit-border-radius: 0 4px 4px 0;
|
||||
-moz-border-radius: 0 4px 4px 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
background: #ccc;
|
||||
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
|
||||
background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
|
||||
background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
|
||||
background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
|
||||
background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#cccccc', endColorstr = '#eeeeee', GradientType = 0);
|
||||
background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%);
|
||||
border-left: 1px solid #aaa;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.select2-container .select2-choice div b {
|
||||
background: url('select2.png') no-repeat 0 1px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.select2-container .select2-search {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
z-index: 1010;
|
||||
min-height: 26px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.select2-container .select2-search-hidden {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
}
|
||||
|
||||
.select2-container .select2-search input {
|
||||
background: #fff url('select2.png') no-repeat 100% -22px;
|
||||
background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
|
||||
background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
||||
background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
||||
background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
|
||||
background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
||||
background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
||||
padding: 4px 20px 4px 5px;
|
||||
outline: 0;
|
||||
border: 1px solid #aaa;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
width:100%;
|
||||
margin:0;
|
||||
height:auto !important;
|
||||
min-height: 26px;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
-webkit-border-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container .select2-search input.select2-active {
|
||||
background: #fff url('spinner.gif') no-repeat 100%;
|
||||
background: url('spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
|
||||
background: url('spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
||||
background: url('spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
|
||||
background: url('spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
|
||||
background: url('spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
||||
background: url('spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
|
||||
}
|
||||
|
||||
|
||||
.select2-container-active .select2-choice,
|
||||
.select2-container-active .select2-choices {
|
||||
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
|
||||
-moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
-o-box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
border: 1px solid #5897fb;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select2-dropdown-open .select2-choice {
|
||||
border: 1px solid #aaa;
|
||||
border-bottom-color: transparent;
|
||||
-webkit-box-shadow: 0 1px 0 #fff inset;
|
||||
-moz-box-shadow : 0 1px 0 #fff inset;
|
||||
-o-box-shadow : 0 1px 0 #fff inset;
|
||||
box-shadow : 0 1px 0 #fff inset;
|
||||
background-color: #eee;
|
||||
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
|
||||
background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
|
||||
background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
|
||||
background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
|
||||
background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
|
||||
background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
|
||||
-webkit-border-bottom-left-radius : 0;
|
||||
-webkit-border-bottom-right-radius: 0;
|
||||
-moz-border-radius-bottomleft : 0;
|
||||
-moz-border-radius-bottomright: 0;
|
||||
border-bottom-left-radius : 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-dropdown-open .select2-choice div {
|
||||
background: transparent;
|
||||
border-left: none;
|
||||
}
|
||||
.select2-dropdown-open .select2-choice div b {
|
||||
background-position: -18px 1px;
|
||||
}
|
||||
|
||||
/* results */
|
||||
.select2-container .select2-results {
|
||||
margin: 4px 4px 4px 0;
|
||||
padding: 0 0 0 4px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
.select2-container .select2-results li {
|
||||
line-height: 80%;
|
||||
padding: 7px 7px 8px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
.select2-container .select2-results .select2-highlighted {
|
||||
background: #3875d7;
|
||||
color: #fff;
|
||||
}
|
||||
.select2-container .select2-results li em {
|
||||
background: #feffde;
|
||||
font-style: normal;
|
||||
}
|
||||
.select2-container .select2-results .select2-highlighted em {
|
||||
background: transparent;
|
||||
}
|
||||
.select2-container .select2-results .select2-no-results {
|
||||
background: #f4f4f4;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
disabled look for already selected choices in the results dropdown
|
||||
.select2-container .select2-results .select2-disabled.select2-highlighted {
|
||||
color: #666;
|
||||
background: #f4f4f4;
|
||||
display: list-item;
|
||||
cursor: default;
|
||||
}
|
||||
.select2-container .select2-results .select2-disabled {
|
||||
background: #f4f4f4;
|
||||
display: list-item;
|
||||
cursor: default;
|
||||
}
|
||||
*/
|
||||
.select2-container .select2-results .select2-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-more-results.select2-active {
|
||||
background: #f4f4f4 url('spinner.gif') no-repeat 100%;
|
||||
}
|
||||
|
||||
.select2-more-results {
|
||||
background: #f4f4f4;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* disabled styles */
|
||||
|
||||
.select2-container.select2-container-disabled .select2-choice {
|
||||
background-color: #f4f4f4;
|
||||
background-image: none;
|
||||
border: 1px solid #ddd;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container.select2-container-disabled .select2-choice div {
|
||||
background-color: #f4f4f4;
|
||||
background-image: none;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* multiselect */
|
||||
|
||||
.select2-container-multi .select2-choices {
|
||||
background-color: #fff;
|
||||
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
|
||||
background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
|
||||
background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
|
||||
background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
|
||||
border: 1px solid #aaa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: text;
|
||||
overflow: hidden;
|
||||
height: auto !important;
|
||||
height: 1%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-drop {
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices {
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.select2-container-multi.select2-container-active .select2-choices {
|
||||
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
|
||||
-moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
-o-box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
box-shadow : 0 0 5px rgba(0,0,0,.3);
|
||||
border: 1px solid #5897fb;
|
||||
outline: none;
|
||||
}
|
||||
.select2-container-multi .select2-choices li {
|
||||
float: left;
|
||||
list-style: none;
|
||||
}
|
||||
.select2-container-multi .select2-choices .select2-search-field {
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-field input {
|
||||
color: #666;
|
||||
background: transparent !important;
|
||||
font-family: sans-serif;
|
||||
font-size: 100%;
|
||||
height: 15px;
|
||||
padding: 5px;
|
||||
margin: 1px 0;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow : none;
|
||||
-o-box-shadow : none;
|
||||
box-shadow : none;
|
||||
}
|
||||
|
||||
|
||||
.select2-default {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice {
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius : 3px;
|
||||
border-radius : 3px;
|
||||
-moz-background-clip : padding;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip : padding-box;
|
||||
background-color: #e4e4e4;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
|
||||
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
|
||||
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
-webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
|
||||
-moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
|
||||
box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
|
||||
color: #333;
|
||||
border: 1px solid #aaaaaa;
|
||||
line-height: 13px;
|
||||
padding: 3px 5px 3px 18px;
|
||||
margin: 3px 0 3px 5px;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
.select2-container-multi .select2-choices .select2-search-choice span {
|
||||
cursor: default;
|
||||
}
|
||||
.select2-container-multi .select2-choices .select2-search-choice-focus {
|
||||
background: #d4d4d4;
|
||||
}
|
||||
|
||||
.select2-search-choice-close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
top: 4px;
|
||||
width: 12px;
|
||||
height: 13px;
|
||||
font-size: 1px;
|
||||
background: url(select2.png) right top no-repeat;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select2-container-multi .select2-search-choice-close {
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
|
||||
.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
|
||||
background-position: right -11px;
|
||||
}
|
||||
.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
|
||||
background-position: right -11px;
|
||||
}
|
||||
|
||||
|
||||
.select2-container-multi .select2-results {
|
||||
margin: -1px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* disabled styles */
|
||||
|
||||
.select2-container-multi.select2-container-disabled .select2-choices{
|
||||
background-color: #f4f4f4;
|
||||
background-image: none;
|
||||
border: 1px solid #ddd;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
|
||||
background-image: none;
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
padding: 3px 5px 3px 5px;
|
||||
}
|
||||
|
||||
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {
|
||||
display: none;
|
||||
}
|
||||
/* end multiselect */
|
29
app/controllers/application_controller.rb
Normal file
29
app/controllers/application_controller.rb
Normal file
|
@ -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
|
75
app/controllers/comments_controller.rb
Normal file
75
app/controllers/comments_controller.rb
Normal file
|
@ -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
|
50
app/controllers/home_controller.rb
Normal file
50
app/controllers/home_controller.rb
Normal file
|
@ -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
|
68
app/controllers/login_controller.rb
Normal file
68
app/controllers/login_controller.rb
Normal file
|
@ -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
|
107
app/controllers/messages_controller.rb
Normal file
107
app/controllers/messages_controller.rb
Normal file
|
@ -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
|
59
app/controllers/signup_controller.rb
Normal file
59
app/controllers/signup_controller.rb
Normal file
|
@ -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
|
225
app/controllers/stories_controller.rb
Normal file
225
app/controllers/stories_controller.rb
Normal file
|
@ -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
|
51
app/controllers/users_controller.rb
Normal file
51
app/controllers/users_controller.rb
Normal file
|
@ -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
|
2
app/helpers/application_helper.rb
Normal file
2
app/helpers/application_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module ApplicationHelper
|
||||
end
|
0
app/mailers/.gitkeep
Normal file
0
app/mailers/.gitkeep
Normal file
11
app/mailers/password_reset.rb
Normal file
11
app/mailers/password_reset.rb
Normal file
|
@ -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
app/models/.gitkeep
Normal file
0
app/models/.gitkeep
Normal file
58
app/models/comment.rb
Normal file
58
app/models/comment.rb
Normal file
|
@ -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
|
43
app/models/keystore.rb
Normal file
43
app/models/keystore.rb
Normal file
|
@ -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
|
186
app/models/story.rb
Normal file
186
app/models/story.rb
Normal file
|
@ -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
|
2
app/models/tag.rb
Normal file
2
app/models/tag.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class Tag < ActiveRecord::Base
|
||||
end
|
4
app/models/tagging.rb
Normal file
4
app/models/tagging.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Tagging < ActiveRecord::Base
|
||||
belongs_to :tag
|
||||
belongs_to :story
|
||||
end
|
40
app/models/user.rb
Normal file
40
app/models/user.rb
Normal file
|
@ -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
|
102
app/models/vote.rb
Normal file
102
app/models/vote.rb
Normal file
|
@ -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
|
2
app/views/global/_footer.html.erb
Normal file
2
app/views/global/_footer.html.erb
Normal file
|
@ -0,0 +1,2 @@
|
|||
<div id="footer">
|
||||
</div>
|
43
app/views/global/_header.html.erb
Normal file
43
app/views/global/_header.html.erb
Normal file
|
@ -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>
|
10
app/views/home/index.html.erb
Normal file
10
app/views/home/index.html.erb
Normal file
|
@ -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>
|
28
app/views/layouts/application.html.erb
Normal file
28
app/views/layouts/application.html.erb
Normal file
|
@ -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>
|
24
app/views/login/forgot_password.html.erb
Normal file
24
app/views/login/forgot_password.html.erb
Normal file
|
@ -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>
|
36
app/views/login/index.html.erb
Normal file
36
app/views/login/index.html.erb
Normal file
|
@ -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>
|
32
app/views/login/set_new_password.html.erb
Normal file
32
app/views/login/set_new_password.html.erb
Normal file
|
@ -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>
|
30
app/views/messages/_list.phtml
Normal file
30
app/views/messages/_list.phtml
Normal file
|
@ -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>
|
23
app/views/messages/compose.phtml
Normal file
23
app/views/messages/compose.phtml
Normal file
|
@ -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>
|
13
app/views/messages/index.phtml
Normal file
13
app/views/messages/index.phtml
Normal file
|
@ -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>
|
69
app/views/messages/show.phtml
Normal file
69
app/views/messages/show.phtml
Normal file
|
@ -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>
|
7
app/views/password_reset/password_reset_link.text.erb
Normal file
7
app/views/password_reset/password_reset_link.text.erb
Normal file
|
@ -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 %>
|
42
app/views/signup/index.html.erb
Normal file
42
app/views/signup/index.html.erb
Normal file
|
@ -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>
|
41
app/views/stories/_comment.html.erb
Normal file
41
app/views/stories/_comment.html.erb
Normal file
|
@ -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>
|
23
app/views/stories/_commentbox.html.erb
Normal file
23
app/views/stories/_commentbox.html.erb
Normal file
|
@ -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 %>
|
84
app/views/stories/_form.html.erb
Normal file
84
app/views/stories/_form.html.erb
Normal file
|
@ -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>
|
55
app/views/stories/_listdetail.html.erb
Normal file
55
app/views/stories/_listdetail.html.erb
Normal file
|
@ -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>
|
13
app/views/stories/edit.phtml
Normal file
13
app/views/stories/edit.phtml
Normal file
|
@ -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>
|
2
app/views/stories/manage.phtml
Normal file
2
app/views/stories/manage.phtml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?= $controller->render(array("partial" => "items/list"),
|
||||
array("items" => $controller->items, "show_caption" => true)); ?>
|
15
app/views/stories/new.html.erb
Normal file
15
app/views/stories/new.html.erb
Normal file
|
@ -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>
|
23
app/views/stories/show.html.erb
Normal file
23
app/views/stories/show.html.erb
Normal file
|
@ -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>
|
51
app/views/users/settings.phtml
Normal file
51
app/views/users/settings.phtml
Normal file
|
@ -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>
|
24
app/views/users/show.phtml
Normal file
24
app/views/users/show.phtml
Normal file
|
@ -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>
|
4
config.ru
Normal file
4
config.ru
Normal file
|
@ -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
|
59
config/application.rb
Normal file
59
config/application.rb
Normal file
|
@ -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
|
6
config/boot.rb
Normal file
6
config/boot.rb
Normal file
|
@ -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'])
|
5
config/environment.rb
Normal file
5
config/environment.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Load the rails application
|
||||
require File.expand_path('../application', __FILE__)
|
||||
|
||||
# Initialize the rails application
|
||||
Lobsters::Application.initialize!
|
37
config/environments/development.rb
Normal file
37
config/environments/development.rb
Normal file
|
@ -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
|
67
config/environments/production.rb
Normal file
67
config/environments/production.rb
Normal file
|
@ -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
|
37
config/environments/test.rb
Normal file
37
config/environments/test.rb
Normal file
|
@ -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
|
7
config/initializers/backtrace_silencers.rb
Normal file
7
config/initializers/backtrace_silencers.rb
Normal file
|
@ -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!
|
15
config/initializers/inflections.rb
Normal file
15
config/initializers/inflections.rb
Normal file
|
@ -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
|
5
config/initializers/mime_types.rb
Normal file
5
config/initializers/mime_types.rb
Normal file
|
@ -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
|
9
config/initializers/session_store.rb
Normal file
9
config/initializers/session_store.rb
Normal file
|
@ -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
|
14
config/initializers/wrap_parameters.rb
Normal file
14
config/initializers/wrap_parameters.rb
Normal file
|
@ -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
|
5
config/locales/en.yml
Normal file
5
config/locales/en.yml
Normal file
|
@ -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"
|
35
config/routes.rb
Normal file
35
config/routes.rb
Normal file
|
@ -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
|
100
db/schema.rb
Normal file
100
db/schema.rb
Normal file
|
@ -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
|
7
db/seeds.rb
Normal file
7
db/seeds.rb
Normal file
|
@ -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)
|
144
extras/markdowner.rb
Normal file
144
extras/markdowner.rb
Normal file
|
@ -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
|
217
extras/sponge.rb
Normal file
217
extras/sponge.rb
Normal file
|
@ -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
|
16
extras/utils.rb
Normal file
16
extras/utils.rb
Normal file
|
@ -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
|
0
log/.gitkeep
Normal file
0
log/.gitkeep
Normal file
26
public/404.html
Normal file
26
public/404.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>The page you were looking for doesn't exist (404)</title>
|
||||
<style type="text/css">
|
||||
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
||||
div.dialog {
|
||||
width: 25em;
|
||||
padding: 0 4em;
|
||||
margin: 4em auto 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
border-right-color: #999;
|
||||
border-bottom-color: #999;
|
||||
}
|
||||
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- This file lives in public/404.html -->
|
||||
<div class="dialog">
|
||||
<h1>The page you were looking for doesn't exist.</h1>
|
||||
<p>You may have mistyped the address or the page may have moved.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
26
public/422.html
Normal file
26
public/422.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>The change you wanted was rejected (422)</title>
|
||||
<style type="text/css">
|
||||
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
||||
div.dialog {
|
||||
width: 25em;
|
||||
padding: 0 4em;
|
||||
margin: 4em auto 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
border-right-color: #999;
|
||||
border-bottom-color: #999;
|
||||
}
|
||||
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- This file lives in public/422.html -->
|
||||
<div class="dialog">
|
||||
<h1>The change you wanted was rejected.</h1>
|
||||
<p>Maybe you tried to change something you didn't have access to.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
25
public/500.html
Normal file
25
public/500.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>We're sorry, but something went wrong (500)</title>
|
||||
<style type="text/css">
|
||||
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
||||
div.dialog {
|
||||
width: 25em;
|
||||
padding: 0 4em;
|
||||
margin: 4em auto 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
border-right-color: #999;
|
||||
border-bottom-color: #999;
|
||||
}
|
||||
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- This file lives in public/500.html -->
|
||||
<div class="dialog">
|
||||
<h1>We're sorry, but something went wrong.</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
5
public/robots.txt
Normal file
5
public/robots.txt
Normal file
|
@ -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: /
|
6
script/rails
Executable file
6
script/rails
Executable file
|
@ -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'
|
104
spec/models/markdowner_spec.rb
Normal file
104
spec/models/markdowner_spec.rb
Normal file
|
@ -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 <pre>" do
|
||||
m " This is some\n text.\n",
|
||||
"<p><pre> This is some\n text.\n</pre></p>"
|
||||
|
||||
m " blah <script>alert('hi');</script>",
|
||||
"<p><pre> blah <script>alert('hi');</script>\n</pre></p>"
|
||||
end
|
||||
|
||||
it "converts text surrounded by * to <em>" do
|
||||
m "oh hullo *there*",
|
||||
"<p>oh hullo <em>there</em></p>"
|
||||
|
||||
m "*hi*",
|
||||
"<p><em>hi</em></p>"
|
||||
|
||||
m "* hi hello*zap zap*",
|
||||
"<p>* hi hello*zap zap*</p>"
|
||||
|
||||
m "oh hullo * there*",
|
||||
"<p>oh hullo * there*</p>"
|
||||
|
||||
m " oh hullo *there*",
|
||||
"<p><pre> oh hullo *there*\n</pre></p>"
|
||||
|
||||
m "oh hullo*there*",
|
||||
"<p>oh hullo*there*</p>"
|
||||
end
|
||||
|
||||
it "converts text surrounded by _ to <u>" do
|
||||
m "oh hullo _there_",
|
||||
"<p>oh hullo <u>there</u></p>"
|
||||
|
||||
m "oh hullo _ there_",
|
||||
"<p>oh hullo _ there_</p>"
|
||||
|
||||
m "oh hullo _there_ and *yes* i see",
|
||||
"<p>oh hullo <u>there</u> and <em>yes</em> i see</p>"
|
||||
end
|
||||
|
||||
it "combines conversions" do
|
||||
m "oh _*hullo*_ there_",
|
||||
"<p>oh <u><em>hullo</em></u> there_</p>"
|
||||
|
||||
m "oh *_hullo_* there_",
|
||||
"<p>oh <em><u>hullo</u></em> there_</p>"
|
||||
|
||||
m "oh *[hello](http://jcs.org/)* there_",
|
||||
"<p>oh <em><a href=\"http://jcs.org/\" rel=\"nofollow\">hello</a>" <<
|
||||
"</em> there_</p>"
|
||||
end
|
||||
|
||||
it "converts domain names to links" do
|
||||
m "oh hullo www.google.com",
|
||||
"<p>oh hullo <a href=\"http://www.google.com\" rel=\"nofollow\">" <<
|
||||
"www.google.com</a></p>"
|
||||
end
|
||||
|
||||
it "converts urls to links" do
|
||||
# no trailing question mark
|
||||
m "do you mean http://jcs.org? or",
|
||||
"<p>do you mean <a href=\"http://jcs.org\" rel=\"nofollow\">" <<
|
||||
"jcs.org</a>? or</p>"
|
||||
|
||||
m "do you mean http://jcs.org?a",
|
||||
"<p>do you mean <a href=\"http://jcs.org?a\" rel=\"nofollow\">" <<
|
||||
"jcs.org?a</a></p>"
|
||||
|
||||
# no trailing dot in url
|
||||
m "i like http://jcs.org.",
|
||||
"<p>i like <a href=\"http://jcs.org\" rel=\"nofollow\">" <<
|
||||
"jcs.org</a>.</p>"
|
||||
|
||||
m "i like http://jcs.org/goose_blah_here",
|
||||
"<p>i like <a href=\"http://jcs.org/goose_blah_here\" " <<
|
||||
"rel=\"nofollow\">jcs.org/goose_blah_here</a></p>"
|
||||
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",
|
||||
"<p>a long <a href=\"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\" rel=\"nofollow\">" <<
|
||||
"www.example.com/goes/here/and/this/is/a/long/url/w...</a></p>"
|
||||
end
|
||||
|
||||
it "converts markdown url format to links" do
|
||||
m "this is a *[link](http://example.com/)*",
|
||||
"<p>this is a <em><a href=\"http://example.com/\" rel=\"nofollow\">" <<
|
||||
"link</a></em></p>"
|
||||
|
||||
m "this is a [link](http://example.com/)",
|
||||
"<p>this is a <a href=\"http://example.com/\" rel=\"nofollow\">" <<
|
||||
"link</a></p>"
|
||||
end
|
||||
end
|
32
spec/spec_helper.rb
Normal file
32
spec/spec_helper.rb
Normal file
|
@ -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
|
7
spec/support/blueprints.rb
Normal file
7
spec/support/blueprints.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require 'machinist/active_record'
|
||||
|
||||
User.blueprint do
|
||||
email { "user-#{sn}@example.com" }
|
||||
password { "blah blah" }
|
||||
password_confirmation { object.password }
|
||||
end
|
0
vendor/assets/javascripts/.gitkeep
vendored
Normal file
0
vendor/assets/javascripts/.gitkeep
vendored
Normal file
0
vendor/assets/stylesheets/.gitkeep
vendored
Normal file
0
vendor/assets/stylesheets/.gitkeep
vendored
Normal file
0
vendor/plugins/.gitkeep
vendored
Normal file
0
vendor/plugins/.gitkeep
vendored
Normal file
Loading…
Reference in a new issue