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