1
0
Fork 0

Artist import, page

This commit is contained in:
Gregory Eremin 2012-08-27 03:53:30 +04:00
parent 235c0b809e
commit b5f616a9d9
36 changed files with 1268 additions and 312 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/config/database.yml
/config/api_keys.yml
/log/*.log
/tmp

View File

@ -8,6 +8,7 @@ group :assets do
gem "coffee-rails", "~> 3.2.1"
gem "therubyracer", platforms: :ruby
gem "hogan_assets"
gem "uglifier", ">= 1.0.3"
end
@ -21,3 +22,9 @@ gem "thin"
# Deploy with Capistrano
gem "capistrano"
gem "robbie", path: "../robbie"
gem "beatparser", path: "../beatparser"
gem "rails-backbone"
gem "eco"
gem 'bootstrap-sass', '~> 2.0.4.0'

View File

@ -1,5 +1,20 @@
PATH
remote: ../beatparser
specs:
beatparser (0.0.1)
lastfm-client
musicbrainz
robbie
PATH
remote: ../robbie
specs:
robbie (0.0.1)
httparty
oj
GEM
remote: https://rubygems.org/
remote: http://rubygems.org/
specs:
actionmailer (3.2.8)
actionpack (= 3.2.8)
@ -29,7 +44,14 @@ GEM
i18n (~> 0.6)
multi_json (~> 1.0)
arel (3.0.2)
bootstrap-sass (2.0.4.0)
builder (3.0.0)
capistrano (2.12.0)
highline
net-scp (>= 1.0.0)
net-sftp (>= 2.0.0)
net-ssh (>= 2.0.14)
net-ssh-gateway (>= 1.1.0)
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
railties (~> 3.2.0)
@ -37,22 +59,53 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.3.3)
daemons (1.1.9)
eco (1.0.0)
coffee-script
eco-source
execjs
eco-source (1.1.0.rc.1)
ejs (1.0.0)
erubis (2.7.0)
eventmachine (0.12.10)
execjs (1.4.0)
multi_json (~> 1.0)
highline (1.6.13)
hike (1.2.1)
hogan_assets (1.3.2)
execjs (>= 1.2.9)
sprockets (>= 2.0.3)
tilt (>= 1.3.3)
httparty (0.8.3)
multi_json (~> 1.0)
multi_xml
i18n (0.6.0)
journey (1.0.4)
jquery-rails (2.1.1)
railties (>= 3.1.0, < 5.0)
thor (~> 0.14)
json (1.7.5)
lastfm-client (0.0.3)
json (>= 1.4.6)
libv8 (3.3.10.4)
mail (2.4.4)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.19)
multi_json (1.3.6)
multi_xml (0.5.1)
musicbrainz (0.7.0)
nokogiri
net-scp (1.0.4)
net-ssh (>= 1.99.1)
net-sftp (2.0.5)
net-ssh (>= 2.0.9)
net-ssh (2.5.2)
net-ssh-gateway (1.1.0)
net-ssh (>= 1.99.1)
nokogiri (1.5.5)
oj (1.3.4)
pg (0.14.0)
polyglot (0.3.3)
rack (1.4.1)
@ -70,6 +123,10 @@ GEM
activesupport (= 3.2.8)
bundler (~> 1.0)
railties (= 3.2.8)
rails-backbone (0.7.2)
coffee-script (~> 2.2.0)
ejs (~> 1.0.0)
railties (>= 3.1.0)
railties (3.2.8)
actionpack (= 3.2.8)
activesupport (= 3.2.8)
@ -89,6 +146,12 @@ GEM
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
therubyracer (0.10.2)
libv8 (~> 3.3.10)
thin (1.4.1)
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thor (0.16.0)
tilt (1.3.3)
treetop (1.4.10)
@ -103,9 +166,18 @@ PLATFORMS
ruby
DEPENDENCIES
beatparser!
bootstrap-sass (~> 2.0.4.0)
capistrano
coffee-rails (~> 3.2.1)
eco
hogan_assets
jquery-rails
pg
rails (= 3.2.8)
rails-backbone
robbie!
sass-rails (~> 3.2.3)
therubyracer
thin
uglifier (>= 1.0.3)

View File

@ -12,4 +12,11 @@
//
//= require jquery
//= require jquery_ujs
//= require mustache
//= require hogan
//= require underscore
//= require backbone
//= require backbone_rails_sync
//= require backbone_datalink
//= require backbone/beat_haven
//= require_tree .

View File

@ -0,0 +1,18 @@
#= require_self
#= require_tree ./templates
#= require_tree ./models
#= require_tree ./views
#= require_tree ./routers
window.BeatHaven =
Models: {}
Collections: {}
Routers: {}
Views: {}
init: ->
new BeatHaven.Routers.Artist()
Backbone.history.start();
$ ->
BeatHaven.init()

View File

@ -0,0 +1,2 @@
class BeatHaven.Models.Artist extends Backbone.Model
urlRoot: "/api/artists"

View File

@ -0,0 +1,9 @@
class BeatHaven.Routers.Artist extends Backbone.Router
routes:
"artist/:name": "show"
show: (name) ->
artist = new BeatHaven.Models.Artist(id: name)
artist.fetch()
view = new BeatHaven.Views.ArtistShow(model: artist)
$("#main").html(view.render().el)

View File

@ -0,0 +1,39 @@
<div class="artist-page">
<h1>{{name}}</h1>
<div class="artist-info">
<div class="pic">
<img src="{{pic}}" alt="{{name}}">
</div>
<div class="bio">{{bio}}</div>
</div>
<div class="albums">
{{#albums}}
<div class="album">
<div class="pic">
<img src="{{pic_safe}}" alt="{{title}}"><br>
<a href="" class="btn btn-success play-all"><i class="icon-plus icon-white"></i> Add to playlist</a>
<a href="" class="btn btn-info play-all"><i class="icon-play icon-white"></i> Play</a>
</div>
<h2 class="title">{{title}} ({{year}})</h2>
<div class="tracks">
<table class="table">
<tbody>
{{#tracks}}
<tr>
<td class="title">
<a href="" class="btn btn-round track-play"><i class="icon-play"></i></a>
<a href="" class="track-link">{{title}}</a>
</td>
<td class="length">
<span class="length">{{length}}</span>
<a href="" class="btn btn-round track-add"><i class="icon-plus"></i></a>
</td>
</tr>
{{/tracks}}
</tbody>
</table>
</div>
</div>
{{/albums}}
</div>
</div>

View File

@ -0,0 +1,10 @@
class BeatHaven.Views.ArtistShow extends Backbone.View
template: HoganTemplates["backbone/templates/artists/show"]
initialize: ->
@model.on("change", @render, this)
render: ->
return this if typeof @model.attributes.id is "string"
$(@el).html(@template.render(@model.toJSON()))
this

View File

@ -1,13 +0,0 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the top of the
* compiled file, but it's generally better to create a new file per style scope.
*
*= require_self
*= require_tree .
*/

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,132 @@
.artist-page {
h1 {
font-family: Lobster, Georgia, serif;
font-size: 42px;
line-height: 64px;
letter-spacing: 1px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
}
.artist-info {
min-height: 250px;
margin-bottom: 50px;
.pic {
float: left;
img {
width: 250px;
height: 250px;
border-size: 1px;
border-style: solid;
border-color: rgba(255, 255, 255, .2);
border-radius: 5px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, .2);
}
}
.bio {
margin-left: 270px;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-size: 22px;
line-height: 26px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
}
}
.album {
min-height: 250px;
margin-bottom: 30px;
h2 {
font-family: Lobster, Georgia, Serif;
font-size: 26px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .2);
}
.pic {
float: left;
height: 300px;
text-align: center;
img {
background-color: #a0a0a0;
width: 250px;
height: 250px;
border-size: 1px;
border-style: solid;
border-color: rgba(255, 255, 255, .2);
border-radius: 5px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, .2);
margin-bottom: 10px;
}
}
.title {
margin-left: 270px;
margin-bottom: 10px;
}
.tracks {
margin-left: 270px;
tr:hover {
td {
background-color: rgba(200, 200, 200, .3);
.track-play i {
opacity: 1;
}
&.length {
.track-add {
display: block;
}
.length {
display: none;
}
}
}
}
td {
position: relative;
font-family: "Source Sans Pro", Helvetica, sans-serif;
font-size: 20px;
line-height: 28px;
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
.btn-round {
width: 10px;
height: 22px;
border-radius: 50px;
i {
margin: 3px 0 0 -1px;
}
}
}
td.title {
padding-left: 40px;
.track-play {
position: absolute;
margin: -2px 0 0 -40px;
i {
opacity: .3;
}
}
.track-link {
color: #303030;
}
}
td.length {
text-align: right;
.track-add {
position: absolute;
right: 8px;
margin: -2px 0 0 0;
display: none;
}
}
}
}
}

View File

@ -0,0 +1,8 @@
module Api
class AlbumsController < ::ApplicationController
def picture
album = Album.find(params[:id])
redirect_to album.load_pic
end
end
end

View File

@ -0,0 +1,10 @@
module Api
class ArtistsController < ::ApplicationController
def show
artist = Artist.with_name(params[:id].gsub("+", " "))
return render json: { fail: true } if artist.nil?
render json: artist.dump_json
end
end
end

View File

@ -1,3 +1,7 @@
class ApplicationController < ActionController::Base
protect_from_forgery
def main
end
end

24
app/models/album.rb Normal file
View File

@ -0,0 +1,24 @@
class Album < ActiveRecord::Base
belongs_to :artist
has_many :tracks
attr_accessible :artist_id, :pic, :rovi_id, :title, :year
def pic_safe
unless pic.nil?
pic
else
"/api/albums/#{id}/picture"
end
end
def load_pic
info = BeatParser::Sources::Lastfm.album_info(artist.name, title)
unless info[:pic].nil?
update_attributes(pic: info[:pic])
info[:pic]
else
"/assets/images/album-dummy.png"
end
end
end

88
app/models/artist.rb Normal file
View File

@ -0,0 +1,88 @@
class Artist < ActiveRecord::Base
has_many :albums
has_many :performers
has_many :tracks, through: :performers
has_many :genres, through: :artist_genres
attr_accessible :bio, :is_group, :name, :pic, :rovi_id
def loaded?
pic? && bio?
end
def dump_json
serialized = to_json(
include: {
albums: {
include: {
tracks: {
methods: [:length],
except: [:created_at, :updated_at, :rovi_id, :album_id]
}
},
except: [:created_at, :updated_at, :rovi_id, :pic],
methods: [:pic_safe]
}
},
except: [:created_at, :updated_at, :rovi_id],
)
end
class << self
def with_name(name)
# DB lookup
artist = find_by_name(name)
return artist unless artist.nil?
# Rovi correction
rovi_artist = Robbie::Artist.find_by_name(name)
return artist if rovi_artist && artist = find_by_rovi_id(rovi_artist.id)
# Parsing artist if ok
import(rovi_artist) if rovi_artist
end
def import(rovi_artist)
data = BeatParser::Aggregator.new.combine(rovi_artist.id)
artist = Artist.find_or_create_by_rovi_id(data[:id])
artist.update_attributes(
name: data[:name],
is_group: data[:is_group],
pic: data[:pic],
bio: data[:bio]
)
data[:albums].each do |album_meta|
album = Album.find_or_create_by_rovi_id(album_meta[:id])
album.update_attributes(
artist_id: artist.id,
title: album_meta[:title],
year: album_meta[:year].to_i
)
album_meta[:tracks].each do |track_meta|
track = Track.find_or_create_by_rovi_id(track_meta[:id])
track.update_attributes(
album_id: album.id,
disc_id: track_meta[:disc_id],
position: track_meta[:position],
title: track_meta[:title],
duration: track_meta[:duration]
)
track_meta[:artists].each do |performer|
performer_artist = Artist.find_or_create_by_rovi_id(performer[:id])
performer_artist.update_attributes(
name: performer[:name]
)
Performer.find_or_create_by_artist_id_and_track_id(performer_artist.id, track.id)
end
end
end
data[:genres].each do |genre_meta|
genre = Genre.find_or_create_by_rovi_id(genre_meta[:id])
genre.update_attributes(
name: genre_meta[:name]
)
ArtistGenre.find_or_create_by_artist_id_and_genre_id(artist.id, genre.id)
end
end
end
end

View File

@ -0,0 +1,3 @@
class ArtistGenre < ActiveRecord::Base
attr_accessible :artist_id, :genre_id
end

3
app/models/genre.rb Normal file
View File

@ -0,0 +1,3 @@
class Genre < ActiveRecord::Base
attr_accessible :name, :rovi_id
end

6
app/models/performer.rb Normal file
View File

@ -0,0 +1,6 @@
class Performer < ActiveRecord::Base
belongs_to :artist
belongs_to :track
attr_accessible :artist_id, :track_id
end

14
app/models/track.rb Normal file
View File

@ -0,0 +1,14 @@
class Track < ActiveRecord::Base
belongs_to :album
has_many :performers
has_many :artists, through: :performers
attr_accessible :album_id, :disc_id, :duration, :position, :rovi_id, :title
def length
return if duration.nil?
length = duration.divmod(60).map(&:to_s)
length[1] = "0" << length[1] if length[1].length == 1
length.join(":")
end
end

View File

View File

@ -2,13 +2,20 @@
<html>
<head>
<title>BeatHaven</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="">BeatHaven</a>
</div>
</div>
</div>
<div class="container" id="main">Loading...</div>
</body>
</html>

View File

@ -0,0 +1,7 @@
lastfm:
api_key: secret
api_secret: secret
client_name: BeatHaven
rovi:
api_key: secret
api_secret: secret

View File

@ -0,0 +1,13 @@
File.open("#{Rails.root}/config/api_keys.yml") do |file|
config = YAML.load(file.read)
LastFM.api_key = config["lastfm"]["api_key"]
LastFM.secret = config["lastfm"]["api_secret"]
LastFM.client_name = config["lastfm"]["client_name"]
Robbie.setup(
api_key: config["rovi"]["api_key"],
api_secret: config["rovi"]["api_secret"]
)
Robbie.enable_cache
end

View File

@ -0,0 +1,3 @@
HoganAssets::Config.configure do |config|
config.path_prefix = "backbone/templates/"
end

View File

@ -1,58 +1,10 @@
BeatHaven::Application.routes.draw do
# The priority is based upon order of creation:
# first created -> highest priority.
namespace :api do
resources :artists, only: [:show], constraints: { id: /.+/ }
resources :albums, only: [:picture] do
member { get :picture }
end
end
# Sample of regular route:
# match 'products/:id' => 'catalog#view'
# Keep in mind you can assign values other than :controller and :action
# Sample of named route:
# match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
# This route can be invoked with purchase_url(:id => product.id)
# Sample resource route (maps HTTP verbs to controller actions automatically):
# resources :products
# Sample resource route with options:
# resources :products do
# member do
# get 'short'
# post 'toggle'
# end
#
# collection do
# get 'sold'
# end
# end
# Sample resource route with sub-resources:
# resources :products do
# resources :comments, :sales
# resource :seller
# end
# Sample resource route with more complex sub-resources
# resources :products do
# resources :comments
# resources :sales do
# get 'recent', :on => :collection
# end
# end
# Sample resource route within a namespace:
# namespace :admin do
# # Directs /admin/products/* to Admin::ProductsController
# # (app/controllers/admin/products_controller.rb)
# resources :products
# end
# You can have the root of your site routed with "root"
# just remember to delete public/index.html.
# root :to => 'welcome#index'
# See how all your routes lay out with "rake routes"
# This is a legacy wild controller route that's not recommended for RESTful applications.
# Note: This route will make all actions in every controller accessible via GET requests.
# match ':controller(/:action(/:id))(.:format)'
root to: "application#main"
end

View File

@ -0,0 +1,10 @@
class CreateGenres < ActiveRecord::Migration
def change
create_table :genres do |t|
t.string :rovi_id
t.string :name
t.timestamps
end
end
end

View File

@ -0,0 +1,10 @@
class CreateArtistGenres < ActiveRecord::Migration
def change
create_table :artist_genres do |t|
t.integer :artist_id
t.integer :genre_id
t.timestamps
end
end
end

View File

@ -0,0 +1,13 @@
class CreateArtists < ActiveRecord::Migration
def change
create_table :artists do |t|
t.string :rovi_id
t.string :name
t.boolean :is_group
t.text :bio
t.string :pic
t.timestamps
end
end
end

View File

@ -0,0 +1,13 @@
class CreateAlbums < ActiveRecord::Migration
def change
create_table :albums do |t|
t.integer :artist_id
t.string :rovi_id
t.string :title
t.integer :year
t.string :pic
t.timestamps
end
end
end

View File

@ -0,0 +1,14 @@
class CreateTracks < ActiveRecord::Migration
def change
create_table :tracks do |t|
t.integer :album_id
t.string :rovi_id
t.integer :disc_id
t.integer :position
t.string :title
t.integer :duration
t.timestamps
end
end
end

View File

@ -0,0 +1,10 @@
class CreatePerformers < ActiveRecord::Migration
def change
create_table :performers do |t|
t.integer :track_id
t.integer :artist_id
t.timestamps
end
end
end

68
db/schema.rb Normal file
View File

@ -0,0 +1,68 @@
# 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 => 20120826172120) do
create_table "albums", :force => true do |t|
t.integer "artist_id"
t.string "rovi_id"
t.string "title"
t.integer "year"
t.string "pic"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "artist_genres", :force => true do |t|
t.integer "artist_id"
t.integer "genre_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "artists", :force => true do |t|
t.string "rovi_id"
t.string "name"
t.boolean "is_group"
t.text "bio"
t.string "pic"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "genres", :force => true do |t|
t.string "rovi_id"
t.string "name"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "performers", :force => true do |t|
t.integer "track_id"
t.integer "artist_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "tracks", :force => true do |t|
t.integer "album_id"
t.string "rovi_id"
t.integer "disc_id"
t.integer "position"
t.string "title"
t.integer "duration"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
end

View File

@ -1,241 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Ruby on Rails: Welcome aboard</title>
<style type="text/css" media="screen">
body {
margin: 0;
margin-bottom: 25px;
padding: 0;
background-color: #f0f0f0;
font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
font-size: 13px;
color: #333;
}
h1 {
font-size: 28px;
color: #000;
}
a {color: #03c}
a:hover {
background-color: #03c;
color: white;
text-decoration: none;
}
#page {
background-color: #f0f0f0;
width: 750px;
margin: 0;
margin-left: auto;
margin-right: auto;
}
#content {
float: left;
background-color: white;
border: 3px solid #aaa;
border-top: none;
padding: 25px;
width: 500px;
}
#sidebar {
float: right;
width: 175px;
}
#footer {
clear: both;
}
#header, #about, #getting-started {
padding-left: 75px;
padding-right: 30px;
}
#header {
background-image: url("assets/rails.png");
background-repeat: no-repeat;
background-position: top left;
height: 64px;
}
#header h1, #header h2 {margin: 0}
#header h2 {
color: #888;
font-weight: normal;
font-size: 16px;
}
#about h3 {
margin: 0;
margin-bottom: 10px;
font-size: 14px;
}
#about-content {
background-color: #ffd;
border: 1px solid #fc0;
margin-left: -55px;
margin-right: -10px;
}
#about-content table {
margin-top: 10px;
margin-bottom: 10px;
font-size: 11px;
border-collapse: collapse;
}
#about-content td {
padding: 10px;
padding-top: 3px;
padding-bottom: 3px;
}
#about-content td.name {color: #555}
#about-content td.value {color: #000}
#about-content ul {
padding: 0;
list-style-type: none;
}
#about-content.failure {
background-color: #fcc;
border: 1px solid #f00;
}
#about-content.failure p {
margin: 0;
padding: 10px;
}
#getting-started {
border-top: 1px solid #ccc;
margin-top: 25px;
padding-top: 15px;
}
#getting-started h1 {
margin: 0;
font-size: 20px;
}
#getting-started h2 {
margin: 0;
font-size: 14px;
font-weight: normal;
color: #333;
margin-bottom: 25px;
}
#getting-started ol {
margin-left: 0;
padding-left: 0;
}
#getting-started li {
font-size: 18px;
color: #888;
margin-bottom: 25px;
}
#getting-started li h2 {
margin: 0;
font-weight: normal;
font-size: 18px;
color: #333;
}
#getting-started li p {
color: #555;
font-size: 13px;
}
#sidebar ul {
margin-left: 0;
padding-left: 0;
}
#sidebar ul h3 {
margin-top: 25px;
font-size: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
#sidebar li {
list-style-type: none;
}
#sidebar ul.links li {
margin-bottom: 5px;
}
.filename {
font-style: italic;
}
</style>
<script type="text/javascript">
function about() {
info = document.getElementById('about-content');
if (window.XMLHttpRequest)
{ xhr = new XMLHttpRequest(); }
else
{ xhr = new ActiveXObject("Microsoft.XMLHTTP"); }
xhr.open("GET","rails/info/properties",false);
xhr.send("");
info.innerHTML = xhr.responseText;
info.style.display = 'block'
}
</script>
</head>
<body>
<div id="page">
<div id="sidebar">
<ul id="sidebar-items">
<li>
<h3>Browse the documentation</h3>
<ul class="links">
<li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
<li><a href="http://api.rubyonrails.org/">Rails API</a></li>
<li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
<li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
</ul>
</li>
</ul>
</div>
<div id="content">
<div id="header">
<h1>Welcome aboard</h1>
<h2>You&rsquo;re riding Ruby on Rails!</h2>
</div>
<div id="about">
<h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
<div id="about-content" style="display: none"></div>
</div>
<div id="getting-started">
<h1>Getting started</h1>
<h2>Here&rsquo;s how to get rolling:</h2>
<ol>
<li>
<h2>Use <code>rails generate</code> to create your models and controllers</h2>
<p>To see all available options, run it without parameters.</p>
</li>
<li>
<h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
<p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
</li>
<li>
<h2>Create your database</h2>
<p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
</li>
</ol>
</div>
</div>
<div id="footer">&nbsp;</div>
</div>
</body>
</html>

613
vendor/assets/javascripts/mustache.js vendored Normal file
View File

@ -0,0 +1,613 @@
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false*/
var Mustache;
(function (exports) {
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = exports; // CommonJS
} else if (typeof define === "function") {
define(exports); // AMD
} else {
Mustache = exports; // <script>
}
}((function () {
var exports = {};
exports.name = "mustache.js";
exports.version = "0.5.2";
exports.tags = ["{{", "}}"];
exports.parse = parse;
exports.clearCache = clearCache;
exports.compile = compile;
exports.compilePartial = compilePartial;
exports.render = render;
exports.Scanner = Scanner;
exports.Context = Context;
exports.Renderer = Renderer;
// This is here for backwards compatibility with 0.4.x.
exports.to_html = function (template, view, partials, send) {
var result = render(template, view, partials);
if (typeof send === "function") {
send(result);
} else {
return result;
}
};
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var nonSpaceRe = /\S/;
var eqRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!/;
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
function testRe(re, string) {
return RegExp.prototype.test.call(re, string);
}
function isWhitespace(string) {
return !testRe(nonSpaceRe, string);
}
var isArray = Array.isArray || function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
};
// OSWASP Guidelines: escape all non alphanumeric characters in ASCII space.
var jsCharsRe = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm;
function quote(text) {
var escaped = text.replace(jsCharsRe, function (c) {
return "\\u" + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
});
return '"' + escaped + '"';
}
function escapeRe(string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
// Export these utility functions.
exports.isWhitespace = isWhitespace;
exports.isArray = isArray;
exports.quote = quote;
exports.escapeRe = escapeRe;
exports.escapeHtml = escapeHtml;
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
if (match && match.index === 0) {
this.tail = this.tail.substring(match[0].length);
this.pos += match[0].length;
return match[0];
}
return "";
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
Scanner.prototype.scanUntil = function (re) {
var match, pos = this.tail.search(re);
switch (pos) {
case -1:
match = this.tail;
this.pos += this.tail.length;
this.tail = "";
break;
case 0:
match = "";
break;
default:
match = this.tail.substring(0, pos);
this.tail = this.tail.substring(pos);
this.pos += pos;
}
return match;
};
function Context(view, parent) {
this.view = view;
this.parent = parent;
this.clearCache();
}
Context.make = function (view) {
return (view instanceof Context) ? view : new Context(view);
};
Context.prototype.clearCache = function () {
this._cache = {};
};
Context.prototype.push = function (view) {
return new Context(view, this);
};
Context.prototype.lookup = function (name) {
var value = this._cache[name];
if (!value) {
if (name === ".") {
value = this.view;
} else {
var context = this;
while (context) {
if (name.indexOf(".") > 0) {
var names = name.split("."), i = 0;
value = context.view;
while (value && i < names.length) {
value = value[names[i++]];
}
} else {
value = context.view[name];
}
if (value != null) {
break;
}
context = context.parent;
}
}
this._cache[name] = value;
}
if (typeof value === "function") {
value = value.call(this.view);
}
return value;
};
function Renderer() {
this.clearCache();
}
Renderer.prototype.clearCache = function () {
this._cache = {};
this._partialCache = {};
};
Renderer.prototype.compile = function (tokens, tags) {
if (typeof tokens === "string") {
tokens = parse(tokens, tags);
}
var fn = compileTokens(tokens),
self = this;
return function (view) {
return fn(Context.make(view), self);
};
};
Renderer.prototype.compilePartial = function (name, tokens, tags) {
this._partialCache[name] = this.compile(tokens, tags);
return this._partialCache[name];
};
Renderer.prototype.render = function (template, view) {
var fn = this._cache[template];
if (!fn) {
fn = this.compile(template);
this._cache[template] = fn;
}
return fn(view);
};
Renderer.prototype._section = function (name, context, callback) {
var value = context.lookup(name);
switch (typeof value) {
case "object":
if (isArray(value)) {
var buffer = "";
for (var i = 0, len = value.length; i < len; ++i) {
buffer += callback(context.push(value[i]), this);
}
return buffer;
}
return value ? callback(context.push(value), this) : "";
case "function":
// TODO: The text should be passed to the callback plain, not rendered.
var sectionText = callback(context, this),
self = this;
var scopedRender = function (template) {
return self.render(template, context);
};
return value.call(context.view, sectionText, scopedRender) || "";
default:
if (value) {
return callback(context, this);
}
}
return "";
};
Renderer.prototype._inverted = function (name, context, callback) {
var value = context.lookup(name);
// From the spec: inverted sections may render text once based on the
// inverse value of the key. That is, they will be rendered if the key
// doesn't exist, is false, or is an empty list.
if (value == null || value === false || (isArray(value) && value.length === 0)) {
return callback(context, this);
}
return "";
};
Renderer.prototype._partial = function (name, context) {
var fn = this._partialCache[name];
if (fn) {
return fn(context);
}
return "";
};
Renderer.prototype._name = function (name, context, escape) {
var value = context.lookup(name);
if (typeof value === "function") {
value = value.call(context.view);
}
var string = (value == null) ? "" : String(value);
if (escape) {
return escapeHtml(string);
}
return string;
};
/**
* Low-level function that compiles the given `tokens` into a
* function that accepts two arguments: a Context and a
* Renderer. Returns the body of the function as a string if
* `returnBody` is true.
*/
function compileTokens(tokens, returnBody) {
var body = ['""'];
var token, method, escape;
for (var i = 0, len = tokens.length; i < len; ++i) {
token = tokens[i];
switch (token.type) {
case "#":
case "^":
method = (token.type === "#") ? "_section" : "_inverted";
body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" +
" " + compileTokens(token.tokens, true) + "\n" +
"})");
break;
case "{":
case "&":
case "name":
escape = token.type === "name" ? "true" : "false";
body.push("r._name(" + quote(token.value) + ", c, " + escape + ")");
break;
case ">":
body.push("r._partial(" + quote(token.value) + ", c)");
break;
case "text":
body.push(quote(token.value));
break;
}
}
// Convert to a string body.
body = "return " + body.join(" + ") + ";";
// Good for debugging.
// console.log(body);
if (returnBody) {
return body;
}
// For great evil!
return new Function("c, r", body);
}
function escapeTags(tags) {
if (tags.length === 2) {
return [
new RegExp(escapeRe(tags[0]) + "\\s*"),
new RegExp("\\s*" + escapeRe(tags[1]))
];
}
throw new Error("Invalid tags: " + tags.join(" "));
}
/**
* Forms the given linear array of `tokens` into a nested tree structure
* where tokens that represent a section have a "tokens" array property
* that contains all tokens that are in that section.
*/
function nestTokens(tokens) {
var tree = [];
var collector = tree;
var sections = [];
var token, section;
for (var i = 0; i < tokens.length; ++i) {
token = tokens[i];
switch (token.type) {
case "#":
case "^":
token.tokens = [];
sections.push(token);
collector.push(token);
collector = token.tokens;
break;
case "/":
if (sections.length === 0) {
throw new Error("Unopened section: " + token.value);
}
section = sections.pop();
if (section.value !== token.value) {
throw new Error("Unclosed section: " + section.value);
}
if (sections.length > 0) {
collector = sections[sections.length - 1].tokens;
} else {
collector = tree;
}
break;
default:
collector.push(token);
}
}
// Make sure there were no open sections when we're done.
section = sections.pop();
if (section) {
throw new Error("Unclosed section: " + section.value);
}
return tree;
}
/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens(tokens) {
var lastToken;
for (var i = 0; i < tokens.length; ++i) {
var token = tokens[i];
if (lastToken && lastToken.type === "text" && token.type === "text") {
lastToken.value += token.value;
tokens.splice(i--, 1); // Remove this token from the array.
} else {
lastToken = token;
}
}
}
/**
* Breaks up the given `template` string into a tree of token objects. If
* `tags` is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
* course, the default is to use mustaches (i.e. Mustache.tags).
*/
function parse(template, tags) {
tags = tags || exports.tags;
var tagRes = escapeTags(tags);
var scanner = new Scanner(template);
var tokens = [], // Buffer to hold the tokens
spaces = [], // Indices of whitespace tokens on the current line
hasTag = false, // Is there a {{tag}} on the current line?
nonSpace = false; // Is there a non-space char on the current line?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
var stripSpace = function () {
if (hasTag && !nonSpace) {
while (spaces.length) {
tokens.splice(spaces.pop(), 1);
}
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
};
var type, value, chr;
while (!scanner.eos()) {
value = scanner.scanUntil(tagRes[0]);
if (value) {
for (var i = 0, len = value.length; i < len; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
nonSpace = true;
}
tokens.push({type: "text", value: chr});
if (chr === "\n") {
stripSpace(); // Check for whitespace on the current line.
}
}
}
// Match the opening tag.
if (!scanner.scan(tagRes[0])) {
break;
}
hasTag = true;
type = scanner.scan(tagRe) || "name";
// Skip any whitespace between tag and value.
scanner.scan(whiteRe);
// Extract the tag value.
if (type === "=") {
value = scanner.scanUntil(eqRe);
scanner.scan(eqRe);
scanner.scanUntil(tagRes[1]);
} else if (type === "{") {
var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
value = scanner.scanUntil(closeRe);
scanner.scan(curlyRe);
scanner.scanUntil(tagRes[1]);
} else {
value = scanner.scanUntil(tagRes[1]);
}
// Match the closing tag.
if (!scanner.scan(tagRes[1])) {
throw new Error("Unclosed tag at " + scanner.pos);
}
tokens.push({type: type, value: value});
if (type === "name" || type === "{" || type === "&") {
nonSpace = true;
}
// Set the tags for the next time around.
if (type === "=") {
tags = value.split(spaceRe);
tagRes = escapeTags(tags);
}
}
squashTokens(tokens);
return nestTokens(tokens);
}
// The high-level clearCache, compile, compilePartial, and render functions
// use this default renderer.
var _renderer = new Renderer();
/**
* Clears all cached templates and partials.
*/
function clearCache() {
_renderer.clearCache();
}
/**
* High-level API for compiling the given `tokens` down to a reusable
* function. If `tokens` is a string it will be parsed using the given `tags`
* before it is compiled.
*/
function compile(tokens, tags) {
return _renderer.compile(tokens, tags);
}
/**
* High-level API for compiling the `tokens` for the partial with the given
* `name` down to a reusable function. If `tokens` is a string it will be
* parsed using the given `tags` before it is compiled.
*/
function compilePartial(name, tokens, tags) {
return _renderer.compilePartial(name, tokens, tags);
}
/**
* High-level API for rendering the `template` using the given `view`. The
* optional `partials` object may be given here for convenience, but note that
* it will cause all partials to be re-compiled, thus hurting performance. Of
* course, this only matters if you're going to render the same template more
* than once. If so, it is best to call `compilePartial` before calling this
* function and to leave the `partials` argument blank.
*/
function render(template, view, partials) {
if (partials) {
for (var name in partials) {
compilePartial(name, partials[name]);
}
}
return _renderer.render(template, view);
}
return exports;
}())));