├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.rdoc ├── Rakefile ├── app ├── assets │ ├── images │ │ └── rails.png │ ├── javascripts │ │ ├── application.js │ │ ├── d3.geom.js │ │ ├── d3.js │ │ ├── d3.layout.js │ │ ├── links.js.coffee │ │ ├── tags.js.coffee │ │ ├── tweets.js.coffee │ │ └── users.js.coffee │ └── stylesheets │ │ ├── application.css │ │ ├── links.css.scss │ │ ├── scaffolds.css.scss │ │ ├── tags.css.scss │ │ ├── tweets.css.scss │ │ └── users.css.scss ├── controllers │ ├── application_controller.rb │ ├── links_controller.rb │ ├── tags_controller.rb │ ├── tweets_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── links_helper.rb │ ├── tags_helper.rb │ ├── tweets_helper.rb │ └── users_helper.rb ├── mailers │ └── .gitkeep ├── models │ ├── .gitkeep │ ├── link.rb │ ├── tag.rb │ ├── tweet.rb │ └── user.rb └── views │ ├── layouts │ └── application.html.erb │ ├── links │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── tags │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── tweets │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ └── users │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── secret_token.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml └── routes.rb ├── db └── seeds.rb ├── doc └── README_FOR_APP ├── lib ├── assets │ └── .gitkeep └── tasks │ └── .gitkeep ├── log └── .gitkeep ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico └── robots.txt ├── script └── rails ├── spec ├── controllers │ ├── links_controller_spec.rb │ ├── tags_controller_spec.rb │ ├── tweets_controller_spec.rb │ └── users_controller_spec.rb ├── helpers │ ├── links_helper_spec.rb │ ├── tags_helper_spec.rb │ ├── tweets_helper_spec.rb │ └── users_helper_spec.rb ├── models │ ├── link_spec.rb │ ├── tag_spec.rb │ ├── tweet_spec.rb │ └── user_spec.rb ├── requests │ ├── links_spec.rb │ ├── tags_spec.rb │ ├── tweets_spec.rb │ └── users_spec.rb ├── routing │ ├── links_routing_spec.rb │ ├── tags_routing_spec.rb │ ├── tweets_routing_spec.rb │ └── users_routing_spec.rb └── views │ ├── links │ ├── edit.html.erb_spec.rb │ ├── index.html.erb_spec.rb │ ├── new.html.erb_spec.rb │ └── show.html.erb_spec.rb │ ├── tags │ ├── edit.html.erb_spec.rb │ ├── index.html.erb_spec.rb │ ├── new.html.erb_spec.rb │ └── show.html.erb_spec.rb │ ├── tweets │ ├── edit.html.erb_spec.rb │ ├── index.html.erb_spec.rb │ ├── new.html.erb_spec.rb │ └── show.html.erb_spec.rb │ └── users │ ├── edit.html.erb_spec.rb │ ├── index.html.erb_spec.rb │ ├── new.html.erb_spec.rb │ └── show.html.erb_spec.rb ├── test ├── fixtures │ └── .gitkeep ├── functional │ └── .gitkeep ├── integration │ └── .gitkeep ├── performance │ └── browsing_test.rb ├── test_helper.rb └── unit │ └── .gitkeep └── vendor ├── assets └── stylesheets │ └── .gitkeep └── plugins └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/ 5 | .sass-cache/ 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails' 4 | gem 'neo4j' 5 | gem 'neo4j-will_paginate' 6 | gem 'will_paginate' 7 | 8 | gem 'twitter', '1.7.2' 9 | 10 | group :development do 11 | gem 'rspec-rails' 12 | end 13 | 14 | 15 | 16 | gem 'jruby-openssl' 17 | gem 'json' 18 | 19 | # Gems used only for assets and not required 20 | # in production environments by default. 21 | group :assets do 22 | gem 'sass-rails', '~> 3.2.3' 23 | gem 'coffee-rails', '~> 3.2.1' 24 | 25 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 26 | gem 'therubyrhino' 27 | 28 | gem 'uglifier', '>= 1.0.3' 29 | end 30 | 31 | gem 'jquery-rails' 32 | 33 | # To use ActiveModel has_secure_password 34 | # gem 'bcrypt-ruby', '~> 3.0.0' 35 | 36 | # To use Jbuilder templates for JSON 37 | # gem 'jbuilder' 38 | 39 | # Use unicorn as the app server 40 | # gem 'unicorn' 41 | 42 | # Deploy with Capistrano 43 | # gem 'capistrano' 44 | 45 | # To use debugger 46 | # gem 'ruby-debug' 47 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | actionmailer (3.2.8) 5 | actionpack (= 3.2.8) 6 | mail (~> 2.4.4) 7 | actionpack (3.2.8) 8 | activemodel (= 3.2.8) 9 | activesupport (= 3.2.8) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | journey (~> 1.0.4) 13 | rack (~> 1.4.0) 14 | rack-cache (~> 1.2) 15 | rack-test (~> 0.6.1) 16 | sprockets (~> 2.1.3) 17 | activemodel (3.2.8) 18 | activesupport (= 3.2.8) 19 | builder (~> 3.0.0) 20 | activerecord (3.2.8) 21 | activemodel (= 3.2.8) 22 | activesupport (= 3.2.8) 23 | arel (~> 3.0.2) 24 | tzinfo (~> 0.3.29) 25 | activeresource (3.2.8) 26 | activemodel (= 3.2.8) 27 | activesupport (= 3.2.8) 28 | activesupport (3.2.8) 29 | i18n (~> 0.6) 30 | multi_json (~> 1.0) 31 | addressable (2.3.2) 32 | arel (3.0.2) 33 | bouncy-castle-java (1.5.0146.1) 34 | builder (3.0.3) 35 | coffee-rails (3.2.2) 36 | coffee-script (>= 2.2.0) 37 | railties (~> 3.2.0) 38 | coffee-script (2.2.0) 39 | coffee-script-source 40 | execjs 41 | coffee-script-source (1.3.3) 42 | diff-lcs (1.1.3) 43 | erubis (2.7.0) 44 | execjs (1.4.0) 45 | multi_json (~> 1.0) 46 | faraday (0.7.6) 47 | addressable (~> 2.2) 48 | multipart-post (~> 1.1) 49 | rack (~> 1.1) 50 | faraday_middleware (0.7.0) 51 | faraday (~> 0.7.3) 52 | hashie (1.1.0) 53 | hike (1.2.1) 54 | i18n (0.6.1) 55 | journey (1.0.4) 56 | jquery-rails (2.1.3) 57 | railties (>= 3.1.0, < 5.0) 58 | thor (~> 0.14) 59 | jruby-openssl (0.7.7) 60 | bouncy-castle-java (>= 1.5.0146.1) 61 | json (1.7.5-java) 62 | mail (2.4.4) 63 | i18n (>= 0.4.0) 64 | mime-types (~> 1.16) 65 | treetop (~> 1.4.8) 66 | mime-types (1.19) 67 | multi_json (1.0.4) 68 | multi_xml (0.4.4) 69 | multipart-post (1.1.5) 70 | neo4j (2.2.0-java) 71 | activemodel (>= 3.0.0, < 3.3) 72 | neo4j-wrapper (= 2.2.0) 73 | orm_adapter (>= 0.0.3) 74 | railties (>= 3.0.0, < 3.3) 75 | neo4j-community (1.8.RC1-java) 76 | neo4j-core (2.2.0-java) 77 | neo4j-community (>= 1.8.M05, < 1.9) 78 | neo4j-cypher (~> 1.0.0) 79 | neo4j-cypher (1.0.0) 80 | neo4j-will_paginate (0.2.0-java) 81 | activesupport (~> 3.0) 82 | neo4j (~> 2.2) 83 | will_paginate (~> 3.0) 84 | neo4j-wrapper (2.2.0-java) 85 | neo4j-core (= 2.2.0) 86 | orm_adapter (0.4.0) 87 | polyglot (0.3.3) 88 | rack (1.4.1) 89 | rack-cache (1.2) 90 | rack (>= 0.4) 91 | rack-ssl (1.3.2) 92 | rack 93 | rack-test (0.6.2) 94 | rack (>= 1.0) 95 | rails (3.2.8) 96 | actionmailer (= 3.2.8) 97 | actionpack (= 3.2.8) 98 | activerecord (= 3.2.8) 99 | activeresource (= 3.2.8) 100 | activesupport (= 3.2.8) 101 | bundler (~> 1.0) 102 | railties (= 3.2.8) 103 | railties (3.2.8) 104 | actionpack (= 3.2.8) 105 | activesupport (= 3.2.8) 106 | rack-ssl (~> 1.3.2) 107 | rake (>= 0.8.7) 108 | rdoc (~> 3.4) 109 | thor (>= 0.14.6, < 2.0) 110 | rake (0.9.2.2) 111 | rdoc (3.12) 112 | json (~> 1.4) 113 | rspec (2.11.0) 114 | rspec-core (~> 2.11.0) 115 | rspec-expectations (~> 2.11.0) 116 | rspec-mocks (~> 2.11.0) 117 | rspec-core (2.11.1) 118 | rspec-expectations (2.11.3) 119 | diff-lcs (~> 1.1.3) 120 | rspec-mocks (2.11.3) 121 | rspec-rails (2.11.0) 122 | actionpack (>= 3.0) 123 | activesupport (>= 3.0) 124 | railties (>= 3.0) 125 | rspec (~> 2.11.0) 126 | sass (3.2.1) 127 | sass-rails (3.2.5) 128 | railties (~> 3.2.0) 129 | sass (>= 3.1.10) 130 | tilt (~> 1.3) 131 | simple_oauth (0.1.9) 132 | sprockets (2.1.3) 133 | hike (~> 1.2) 134 | rack (~> 1.0) 135 | tilt (~> 1.1, != 1.3.0) 136 | therubyrhino (2.0.1) 137 | therubyrhino_jar (>= 1.7.3) 138 | therubyrhino_jar (1.7.4) 139 | thor (0.16.0) 140 | tilt (1.3.3) 141 | treetop (1.4.10) 142 | polyglot 143 | polyglot (>= 0.3.1) 144 | twitter (1.7.2) 145 | faraday (~> 0.7.4) 146 | faraday_middleware (~> 0.7.0) 147 | hashie (~> 1.1.0) 148 | multi_json (~> 1.0.0) 149 | multi_xml (~> 0.4.0) 150 | simple_oauth (~> 0.1.5) 151 | tzinfo (0.3.33) 152 | uglifier (1.3.0) 153 | execjs (>= 0.3.0) 154 | multi_json (~> 1.0, >= 1.0.2) 155 | will_paginate (3.0.3) 156 | 157 | PLATFORMS 158 | java 159 | 160 | DEPENDENCIES 161 | coffee-rails (~> 3.2.1) 162 | jquery-rails 163 | jruby-openssl 164 | json 165 | neo4j 166 | neo4j-will_paginate 167 | rails 168 | rspec-rails 169 | sass-rails (~> 3.2.3) 170 | therubyrhino 171 | twitter (= 1.7.2) 172 | uglifier (>= 1.0.3) 173 | will_paginate 174 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == Neo4j.rb example application 2 | 3 | This application shows how to use neo4j together with rails 3.1 by parsing Twitter feeds and creating a social graph. 4 | It first shows some basic operations which you are probably used to from the ActiveRecord/ActiveModel API. 5 | In the last section we take real advantage of the neo4j engine by implementing a recommendation algorithm for finding new twitter users. 6 | 7 | Have fun and feel free to clone it. 8 | 9 | === Installation 10 | 11 | Make sure you have Java JDK 1.6+ installed 12 | 13 | Install latest JRuby, example 14 | rvm use jruby-1.6.7 15 | 16 | Install Rails (>= 3.1.1) 17 | gem install rails 18 | 19 | === 1. Create a rails project 20 | 21 | Use my rails template which will disable active record and enable neo4j instead 22 | rails new kvitter -m http://andreasronge.github.com/neo4j/rails.rb 23 | 24 | Edit the Gemfile and add the twitter gem 25 | cd kvitter 26 | emacs Gemfile # and add 27 | gem 'twitter', '1.7.2' 28 | 29 | Download all the dependencies 30 | bundle 31 | 32 | === 2. Scaffold 33 | 34 | Run the following commands: 35 | 36 | rails generate scaffold Tweet text:string link:string date:datetime tweet_id:string --indices tweet_id date text --has_n tags mentions links --has_one tweeted_by:tweeted 37 | rails generate scaffold User twid:string link:string --indices twid --has_n tweeted follows knows used_tags mentioned_from:mentions 38 | rails generate scaffold Link url:string --indices url --has_n tweets:links short_urls:redirected_link --has_one redirected_link 39 | rails generate scaffold Tag name:string --indices name --has_n tweets:tags used_by_users:used_tags 40 | 41 | There is nothing magical happening here like neo4j configuration or migrations. 42 | It only creates plain new Ruby classes (models, controllers and views) and routing. 43 | The relationships and properties are only specified in the model classes. 44 | 45 | === 3. Start Rails 46 | 47 | Test the basic crud operations 48 | rails s 49 | 50 | Open browser: http://localhost:3000/tags 51 | 52 | === 4 Search using Twitter API 53 | 54 | Add the following: 55 | 56 | ==== app/controllers/tag_controller.rb: 57 | 58 | The following code does a twitter search and parses the result. 59 | It creates and connects the Tweet, Link, User and Tag model classes. 60 | 61 | def search 62 | @tag = Tag.find(params[:id]) 63 | 64 | search = Twitter::Search.new 65 | result = search.hashtag(@tag.name) 66 | 67 | curr_page = 0 68 | while curr_page < 2 do 69 | result.each do |item| 70 | parsed_tweet_hash = Tweet.parse(item) 71 | next if Tweet.find_by_tweet_id(parsed_tweet_hash[:tweet_id]) 72 | tweet = Tweet.create!(parsed_tweet_hash) 73 | 74 | twid = item['from_user'].downcase 75 | user = User.find_or_create_by(:twid => twid) 76 | user.tweeted << tweet 77 | user.save 78 | 79 | parse_tweet(tweet, user) 80 | end 81 | result.fetch_next_page 82 | curr_page += 1 83 | end 84 | 85 | redirect_to @tag 86 | end 87 | 88 | 89 | def parse_tweet(tweet, user) 90 | tweet.text.gsub(/(@\w+|https?:\/\/[a-zA-Z0-9\-\.~\:\?#\[\]\!\@\$&,\*+=;,\/]+|#\w+)/).each do |t| 91 | case t 92 | when /^@.+/ 93 | t = t[1..-1].downcase 94 | next if t.nil? 95 | other = User.find_or_create_by(:twid => t) 96 | user.knows << other unless t == user.twid || user.knows.include?(other) 97 | user.save 98 | tweet.mentions << other 99 | when /#.+/ 100 | t = t[1..-1].downcase 101 | tag = Tag.find_or_create_by(:name => t) 102 | tweet.tags << tag unless tweet.tags.include?(tag) 103 | user.used_tags << tag unless user.used_tags.include?(tag) 104 | user.save 105 | when /https?:.+/ 106 | link = Link.find_or_create_by(:url => t) 107 | tweet.links << (link.redirected_link || link) 108 | end 109 | end 110 | tweet.save! 111 | end 112 | 113 | 114 | ==== app/models/tweet.rb: 115 | 116 | Change index on text to fulltext lucene: 117 | 118 | property :text, :type => String, :index => :fulltext 119 | 120 | Add a to_s and parse method and change the index type of text to fulltext (we need that later on, see below). 121 | 122 | def to_s 123 | text.gsub(/(@\w+|https?\S+|#\w+)/,"").strip 124 | end 125 | 126 | def self.parse(item) 127 | {:tweet_id => item['id_str'], 128 | :text => item['text'], 129 | :date => Time.parse(item['created_at']), 130 | :link => "http://twitter.com/#{item['from_user']}/statuses/#{item['id_str']}" 131 | } 132 | end 133 | 134 | Notice : in Neo4j it is not necessary to specify the types of properties. By setting :type => String we force that each Tweet object will store the property 'type' as strings. 135 | 136 | ==== config/routes.rb: 137 | 138 | change 139 | resource :tags 140 | to 141 | resources :tags do 142 | get :search, :on => :member 143 | end 144 | 145 | ==== app/views/tags/show.html.erb 146 | 147 | Add a button to the view: 148 | 149 | <%= button_to "Search", [:search, @tag], :method => :get %> 150 | 151 | Test the application now by opening a browser http://localhost:3000/tags 152 | Create a new tag 153 | and press the button 'search' 154 | You will now found tweets, users and links 155 | 156 | === Follow URL shortenings 157 | 158 | When you looked at all the links, most of them are short urls like http://t.co 159 | Since we are more interested in the real link and who has tweeted about them 160 | we must follow the URL by doing a HTTP head request. 161 | 162 | We use a before save callback to create a the real link which correspond to where 163 | the short url is directed to. The short link and the real link are connected in a redirected_link relationship. 164 | 165 | 166 | ==== app/models/link.rb 167 | class Link < Neo4j::Rails::Model 168 | property :url, :type => String, :index => :exact 169 | has_n(:tweets).from(:links) 170 | has_n(:short_urls).from(:redirected_link) 171 | has_one :redirected_link 172 | 173 | # Add the following: 174 | before_save :create_redirect_link 175 | 176 | SHORT_URLS = %w[t.co bit.ly ow.ly goo.gl tiny.cc tinyurl.com doiop.com readthisurl.com memurl.com tr.im cli.gs short.ie kl.am idek.net short.ie is.gd hex.io asterl.in j.mp].to_set 177 | 178 | def to_s 179 | url 180 | end 181 | 182 | private 183 | 184 | def self.short_url?(url) 185 | domain = url.split('/')[2] 186 | domain && SHORT_URLS.include?(domain) 187 | end 188 | 189 | def create_redirect_link 190 | return if !self.class.short_url?(url) 191 | uri = URI.parse(url) 192 | http = Net::HTTP.new(uri.host, uri.port) 193 | http.read_timeout = 200 194 | req = Net::HTTP::Head.new(uri.request_uri) 195 | res = http.request(req) 196 | redirect = res['location'] 197 | if redirect && url != redirect 198 | self.redirected_link = Link.find_or_create_by(:url => redirect.strip) 199 | end 200 | rescue Timeout::Error 201 | puts "Can't acccess #{url}" 202 | rescue Error 203 | puts "Can't call #{url}" 204 | rescue Net::HTTPBadResponse 205 | puts "Bad response for #{url}" 206 | end 207 | 208 | ==== app/views/links/show.html.erb 209 | 210 | In order to show both outgoing redirected_link and incoming redirected_link (by using short_urls method) add the following: 211 | 212 |

213 | Short Urls: 214 | <% @link.short_urls.each do |link| %> 215 | <%= link_to link, link %>
216 | <% end %> 217 |

218 | 219 | <% if @link.redirected_link %> 220 |

221 | Redirects to 222 | <%= link_to @link.redirected_link, @link.redirected_link %> 223 |

224 | <% end %> 225 | 226 | === Don't display URL shortening 227 | 228 | The index page shows all the links, including links like http://bit.ly and the the real one. 229 | To only return the real URLs we can use rules, which is a bit similar to scope in ActiveRecord. 230 | 231 | ==== app/models/links.rb 232 | 233 | rule(:real) { redirected_link.nil?} 234 | 235 | This means that it will group all links under the rule :real which does not have a redirected_link 236 | To return all those nodes, use the class method #real. Notice you can do some really interesting queries using rules and cypher together, see "Rules-Cypher":https://github.com/andreasronge/neo4j/wiki/Neo4j%3A%3AWrapper-Rules-and-Functions 237 | 238 | Btw, the Neo4j::Rails::Model#all method is also implemented as a rule. 239 | You can also chain rules, just like scopes in Active Record. 240 | 241 | ==== app/controllers/links_controller.rb 242 | 243 | def index 244 | @links = Link.real 245 | 246 | respond_to do |format| 247 | format.html # index.html.erb 248 | format.json { render :json => @links } 249 | end 250 | end 251 | 252 | Since the rules do work by creating relationships when new nodes are created/updated/deleted we must do another 253 | search or stop the rails server and delete the database 'db/neo4j-developement' 254 | 255 | === Pagination 256 | 257 | The list of all tweets (http://localhost:3000/tweets ) does not look good. It needs some pagination. 258 | Add the two gem in Gemfile: 259 | 260 | gem 'neo4j-will_paginate', :git => 'git://github.com/andreasronge/neo4j-will_paginate.git' 261 | gem 'will_paginate' 262 | 263 | ==== app/controllers/tweet_controller.rb 264 | 265 | Neo4j.rb comes included with the will_paginate gem. 266 | Change the index method 267 | 268 | @tweets = Tweet.all 269 | to 270 | @tweets = Tweet.all.paginate(:page => params[:page], :per_page => 10) 271 | 272 | Pagination is support for all traversals and lucene queries. 273 | 274 | ==== app/views/tweets/index.html.erb 275 | 276 | Add the following line before the table: 277 | 278 | <%= will_paginate(@tweets) %> 279 | 280 | === Searching and Sorting 281 | 282 | Lets say we want to sort the tweets by the text. Lucene has two types of indexes: exact and fulltext. 283 | The exact index is perfect for keywords while the fulltext is for longer texts. 284 | We have already changed the index of text to fulltext for the app/models/tweets.rb 285 | See http://lucene.apache.org/java/3_0_0/queryparsersyntax.html for the query syntax. 286 | 287 | ==== app/views/tweets/index.html.erb 288 | 289 | Add the following form before the table 290 | 291 | <%= form_for(:tweets, :method => :get) do |f| %> 292 |
293 | <%= text_field_tag :query %> 294 | <%= f.submit "Search" %> 295 |
296 | <% end %> 297 | 298 | 299 | ==== app/controllers/tweets_controller.rb 300 | 301 | In the index method we now should handle the query parameter. 302 | Add the following before the respond_to. 303 | 304 | def index 305 | query = params[:query] 306 | if query.present? 307 | @tweets = Tweet.all("text:#{query}", :type => :fulltext).paginate(:page => params[:page], :per_page => 10) 308 | else 309 | @tweets = Tweet.all.paginate(:page => params[:page], :per_page => 10) 310 | end 311 | 312 | # respond_to ... 313 | end 314 | 315 | Test it ! 316 | 317 | 318 | === Add SVG Visualization ! 319 | 320 | Would it not be cool to visualize who knows who ? 321 | 322 | ==== Download the D3 Javascript library 323 | 324 | Download the D3 version 2.4.6 javascript library from https://github.com/mbostock/d3/archives/master. 325 | 326 | Unzip it and move the following files to the app/assets/javascript folder 327 | * d3.geom.js 328 | * d3.js 329 | * d3.layout.js 330 | 331 | ==== app/controllers/users_controller.rb 332 | 333 | Create a JSON API needed by the javascript 334 | 335 | def index 336 | @users = User.all 337 | 338 | respond_to do |format| 339 | format.html # index.html.erb 340 | format.json do 341 | nodes = @users.map{|u| {:name => u.twid, :value => u.tweeted.size}} 342 | links = [] 343 | @users.each do |user| 344 | links += user.knows.map {|other| { :source => nodes.find_index{|n| n[:name] == user.twid}, :target => nodes.find_index{|n| n[:name] == other.twid}}} 345 | end 346 | render :json => {:nodes => nodes, :links => links} 347 | end 348 | end 349 | end 350 | 351 | Test the API, open a browser http://localhost:3000/users.json 352 | it should return something like this: 353 | 354 | {"nodes":[{"name":"geoaxis","value":1},{"name":"peterneubauer","value":1},{"name":"dolugen","value":1},{"name":"neo4j","value":8},{"name":"emileifrem","value":1},{"name":"linnbar_consult","value":1},{"name":"skillsmatter","value":0},{"name":"swgoof","value":1},{"name":"bytor99999","value":0},{"name":"teropaananen","value":1},{"name":"tobyorourke","value":1},{"name":"sannegrinovero","value":1},{"name":"emmanuelbernard","value":0},{"name":"jimwebber","value":0},{"name":"ianrobinson","value":0},{"name":"mesirii","value":1},{"name":"kings13y","value":0},{"name":"chitrasen","value":1},{"name":"einarhreindal","value":1},{"name":"smunchang","value":1},{"name":"technige","value":1},{"name":"josh_adell","value":1},{"name":"mmuekk","value":1},{"name":"asbkar","value":1},{"name":"noppanit","value":1},{"name":"roddare","value":1},{"name":"tanyaespe","value":1},{"name":"erikusaj","value":1},{"name":"druidjaidan","value":1}],"links":[{"source":0,"target":1},{"source":2,"target":3},{"source":2,"target":4},{"source":3,"target":7},{"source":3,"target":4},{"source":3,"target":8},{"source":3,"target":9},{"source":3,"target":10},{"source":3,"target":11},{"source":3,"target":12},{"source":3,"target":13},{"source":3,"target":14},{"source":3,"target":1},{"source":3,"target":16},{"source":4,"target":9},{"source":5,"target":6},{"source":7,"target":4},{"source":9,"target":4},{"source":10,"target":4},{"source":11,"target":12},{"source":11,"target":13},{"source":11,"target":14},{"source":15,"target":9},{"source":15,"target":4},{"source":17,"target":9},{"source":17,"target":4},{"source":18,"target":6},{"source":19,"target":4},{"source":19,"target":3},{"source":20,"target":21},{"source":22,"target":6},{"source":23,"target":6},{"source":24,"target":1},{"source":25,"target":1},{"source":26,"target":4},{"source":26,"target":3},{"source":27,"target":6},{"source":28,"target":6}]} 355 | 356 | 357 | 358 | ==== app/assets/javascripts/users.js 359 | 360 | Add the following coffeescript (be careful with the indentation): 361 | 362 | # Place all the behaviors and hooks related to the matching controller here. 363 | # All this logic will automatically be available in application.js. 364 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 365 | 366 | $(document).ready -> 367 | w = 1260 368 | h = 1300 369 | fill = d3.scale.category20() 370 | 371 | vis = d3.select("#graph").append("svg:svg").attr("width", w).attr("height", h) 372 | 373 | d3.json("/users.json", (json) -> 374 | force = d3.layout.force() 375 | .charge(-320) 376 | .linkDistance(160) 377 | .nodes(json.nodes) 378 | .links(json.links) 379 | .size([w, h]) 380 | .start() 381 | 382 | link = vis.selectAll("line.link") 383 | .data(json.links) 384 | .enter().append("svg:line") 385 | .attr("class", "link") 386 | .style("stroke-width", (d) -> Math.sqrt(d.value)) 387 | .attr("x1", (d) -> d.source.x) 388 | .attr("y1", (d) -> d.source.y) 389 | .attr("x2", (d) -> d.target.x) 390 | .attr("y2", (d) -> d.target.y) 391 | 392 | node = vis.selectAll("g.node") 393 | .data(json.nodes) 394 | .enter().append("svg:g") 395 | .attr("transform", (d) -> "translate(" + d.x + "," + d.y + ")") 396 | .attr("class", "node") 397 | .call(force.drag) 398 | 399 | node.append("svg:circle") 400 | .attr("r", (d) -> if d.value > 25 then 50 else d.value*2 + 5) 401 | .style("fill", (d) -> '#fea') 402 | 403 | node.append("svg:title").text((d) -> d.name) 404 | node.append("svg:text") 405 | .attr("text-anchor", "middle") 406 | .attr("dy", ".3em") 407 | .text((d) -> d.name) 408 | 409 | vis.style("opacity", 1e-6) 410 | .transition() 411 | .duration(0) 412 | .style("opacity", 1) 413 | 414 | force.on("tick", -> 415 | link.attr("x1", (d) -> d.source.x).attr("y1", (d) -> d.source.y).attr("x2", (d) -> d.target.x).attr("y2", (d) -> d.target.y) 416 | node.attr("transform", (d) -> "translate(" + d.x + "," + d.y + ")") 417 | ) 418 | ) 419 | ==== app/views/users/index.html.erb 420 | 421 | Add the following at the bottom of the file 422 | 423 |
424 | 425 | 426 | ==== app/assets/javascripts/application.js 427 | 428 | Make sure things are loading in the correct order. 429 | 430 | //= require jquery 431 | //= require jquery_ujs 432 | //= require d3 433 | //= require d3.geom 434 | //= require d3.layout 435 | //= require users 436 | 437 | 438 | ==== app/assets/stylesheets/application.css 439 | 440 | circle.node { 441 | stroke: #fff; 442 | stroke-width: 1.5px; 443 | } 444 | 445 | line.link { 446 | stroke: #999; 447 | stroke-opacity: .6; 448 | } 449 | 450 | ==== Test it 451 | 452 | Open a browser http://localhost:3000/users and scroll down 453 | 454 | 455 | === Better Navigation for Users 456 | 457 | ==== app/views/users/show.erb 458 | Add the following before the link_to lines 459 | 460 |

461 | Used tags 462 | <% @user.used_tags.each do |tag| %> 463 | <%= link_to tag.name, tag %>
464 | <% end %> 465 |

466 | 467 | 468 |

469 | Knows:
470 | <% @knows.each do |user| %> 471 | <%= link_to user, user %>
472 | <% end %> 473 | <%= will_paginate(@knows) %> 474 |

475 | 476 |

477 | Mentioned from:
478 | <% @mentioned_from.each do |tweet| %> 479 | <%= link_to tweet, tweet %>
480 | <% end %> 481 |

482 | 483 |

484 | Tweets
485 | <% @user.tweeted.each do |tweet| %> 486 | <%= link_to tweet, tweet %>
487 | <% end %> 488 |

489 | 490 | 491 | ==== app/controllers/users_controller.rb 492 | 493 | Change the show function to: 494 | 495 | def show 496 | @user = User.find(params[:id]) 497 | @knows = @user.knows.paginate(:page => params[:page], :per_page => 10) 498 | 499 | @mentioned_from = @user.mentioned_from 500 | 501 | respond_to do |format| 502 | format.html # show.html.erb 503 | format.json { render :json => @user } 504 | end 505 | end 506 | 507 | Notice that we do pagination of a traversal (knows). 508 | 509 | === Better Navigation for Tweets 510 | 511 | ==== app/views/tweets/show.html.rb 512 | 513 | Show the relationships from and to a tweet: 514 | 515 |

516 | Text: 517 | <%= @tweet.text %> 518 |

519 | 520 |

521 | Link: 522 | <%= link_to @tweet.link, @tweet.link %> 523 |

524 | 525 |

526 | Date: 527 | <%= @tweet.date %> 528 |

529 | 530 |

531 | Tweet: 532 | <%= @tweet.tweet_id %> 533 |

534 | 535 | 536 |

537 | Tweeted by 538 | <%= link_to @tweet.tweeted_by, @tweet.tweeted_by %> 539 |

540 | 541 |

542 | Tags:
543 | <% @tweet.tags.each do |tag| %> 544 | <%= link_to tag.name, tag %>
545 | <% end %> 546 |

547 | 548 | 549 |

550 | Links:
551 | <% @tweet.links.each do |link| %> 552 | <%= link_to link, link %>
553 | <% end %> 554 |

555 | 556 | 557 | === Better Navigation for Tags 558 | 559 | ==== app/views/tags/show.html.erb 560 | 561 |

562 | Tweets:
563 | <% @tweets.each do |tweet| %> 564 | <%= link_to tweet, tweet %>
565 | <% end %> 566 | <%= will_paginate(@tweets) %> 567 |

568 | 569 | ==== app/controller/ 570 | 571 | Add the following line in the show method after the Tag.find line 572 | 573 | @tweets = @tag.tweets.paginate(:page => params[:page], :per_page => 10) 574 | 575 | === Recommendations 576 | 577 | ==== app/controllers/user_controllers.rb 578 | 579 | The following algorithm works like this: 580 | 581 | 1. Get all the users who are also using my tags. 582 | 2. For each of those users get their used tags and compare with mine. 583 | 3. Select the user who has the most similar tags to mine. 584 | 585 | Add the following at the bottom of the file user_controllers.rb 586 | 587 | private 588 | 589 | def recommend(user) 590 | my_tags = user.used_tags.to_a 591 | my_friends = user.knows.to_a 592 | 593 | # we are here using the raw java API - that's why using _java_node, raw and wrapper 594 | other_users = user._java_node.outgoing(:used_tags).incoming(:used_tags).raw.depth(2).filter{|path| path.length == 2 && !my_friends.include?(path.end_node)} 595 | 596 | # for all those users, find the person who has the max number of same tags as I have 597 | found = other_users.max_by{|friend| (friend.outgoing(:used_tags).raw.map{|tag| tag[:name]} & my_tags).size } 598 | 599 | found && found.wrapper # load the ruby wrapper around the neo4j java node 600 | end 601 | 602 | ==== app/controllers/user_controllers.rb 603 | 604 | Add one line to the show method: 605 | 606 | def show 607 | @user = User.find(params[:id]) 608 | @recommend = recommend(@user) 609 | 610 | respond_to do |format| 611 | format.html # show.html.erb 612 | format.json { render :json => @user } 613 | end 614 | end 615 | 616 | 617 | ==== app/views/users/show.html.erb 618 | 619 | Display a recommendation if available 620 | 621 | <% if @recommend %> 622 |

623 | Recommend 624 | <%= link_to @recommend.twid, @recommend %> 625 |

626 | <% end %> 627 | 628 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Kvitter::Application.load_tasks 8 | -------------------------------------------------------------------------------- /app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/app/assets/images/rails.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into including all the files listed below. 2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically 3 | // be included in the compiled file accessible from http://example.com/assets/application.js 4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 5 | // the compiled file. 6 | // 7 | //= require jquery 8 | //= require jquery_ujs 9 | //= require d3 10 | //= require d3.geom 11 | //= require d3.layout 12 | //= require users 13 | 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/d3.geom.js: -------------------------------------------------------------------------------- 1 | (function(){d3.geom = {}; 2 | /** 3 | * Computes a contour for a given input grid function using the marching 5 | * squares algorithm. Returns the contour polygon as an array of points. 6 | * 7 | * @param grid a two-input function(x, y) that returns true for values 8 | * inside the contour and false for values outside the contour. 9 | * @param start an optional starting point [x, y] on the grid. 10 | * @returns polygon [[x1, y1], [x2, y2], …] 11 | */ 12 | d3.geom.contour = function(grid, start) { 13 | var s = start || d3_geom_contourStart(grid), // starting point 14 | c = [], // contour polygon 15 | x = s[0], // current x position 16 | y = s[1], // current y position 17 | dx = 0, // next x direction 18 | dy = 0, // next y direction 19 | pdx = NaN, // previous x direction 20 | pdy = NaN, // previous y direction 21 | i = 0; 22 | 23 | do { 24 | // determine marching squares index 25 | i = 0; 26 | if (grid(x-1, y-1)) i += 1; 27 | if (grid(x, y-1)) i += 2; 28 | if (grid(x-1, y )) i += 4; 29 | if (grid(x, y )) i += 8; 30 | 31 | // determine next direction 32 | if (i === 6) { 33 | dx = pdy === -1 ? -1 : 1; 34 | dy = 0; 35 | } else if (i === 9) { 36 | dx = 0; 37 | dy = pdx === 1 ? -1 : 1; 38 | } else { 39 | dx = d3_geom_contourDx[i]; 40 | dy = d3_geom_contourDy[i]; 41 | } 42 | 43 | // update contour polygon 44 | if (dx != pdx && dy != pdy) { 45 | c.push([x, y]); 46 | pdx = dx; 47 | pdy = dy; 48 | } 49 | 50 | x += dx; 51 | y += dy; 52 | } while (s[0] != x || s[1] != y); 53 | 54 | return c; 55 | }; 56 | 57 | // lookup tables for marching directions 58 | var d3_geom_contourDx = [1, 0, 1, 1,-1, 0,-1, 1,0, 0,0,0,-1, 0,-1,NaN], 59 | d3_geom_contourDy = [0,-1, 0, 0, 0,-1, 0, 0,1,-1,1,1, 0,-1, 0,NaN]; 60 | 61 | function d3_geom_contourStart(grid) { 62 | var x = 0, 63 | y = 0; 64 | 65 | // search for a starting point; begin at origin 66 | // and proceed along outward-expanding diagonals 67 | while (true) { 68 | if (grid(x,y)) { 69 | return [x,y]; 70 | } 71 | if (x === 0) { 72 | x = y + 1; 73 | y = 0; 74 | } else { 75 | x = x - 1; 76 | y = y + 1; 77 | } 78 | } 79 | } 80 | /** 81 | * Computes the 2D convex hull of a set of points using Graham's scanning 82 | * algorithm. The algorithm has been implemented as described in Cormen, 83 | * Leiserson, and Rivest's Introduction to Algorithms. The running time of 84 | * this algorithm is O(n log n), where n is the number of input points. 85 | * 86 | * @param vertices [[x1, y1], [x2, y2], …] 87 | * @returns polygon [[x1, y1], [x2, y2], …] 88 | */ 89 | d3.geom.hull = function(vertices) { 90 | if (vertices.length < 3) return []; 91 | 92 | var len = vertices.length, 93 | plen = len - 1, 94 | points = [], 95 | stack = [], 96 | i, j, h = 0, x1, y1, x2, y2, u, v, a, sp; 97 | 98 | // find the starting ref point: leftmost point with the minimum y coord 99 | for (i=1; i= (x2*x2 + y2*y2)) { 129 | points[i].index = -1; 130 | } else { 131 | points[u].index = -1; 132 | a = points[i].angle; 133 | u = i; 134 | v = j; 135 | } 136 | } else { 137 | a = points[i].angle; 138 | u = i; 139 | v = j; 140 | } 141 | } 142 | 143 | // initialize the stack 144 | stack.push(h); 145 | for (i=0, j=0; i<2; ++j) { 146 | if (points[j].index !== -1) { 147 | stack.push(points[j].index); 148 | i++; 149 | } 150 | } 151 | sp = stack.length; 152 | 153 | // do graham's scan 154 | for (; j 0; 177 | } 178 | // Note: requires coordinates to be counterclockwise and convex! 179 | d3.geom.polygon = function(coordinates) { 180 | 181 | coordinates.area = function() { 182 | var i = 0, 183 | n = coordinates.length, 184 | a = coordinates[n - 1][0] * coordinates[0][1], 185 | b = coordinates[n - 1][1] * coordinates[0][0]; 186 | while (++i < n) { 187 | a += coordinates[i - 1][0] * coordinates[i][1]; 188 | b += coordinates[i - 1][1] * coordinates[i][0]; 189 | } 190 | return (b - a) * .5; 191 | }; 192 | 193 | coordinates.centroid = function(k) { 194 | var i = -1, 195 | n = coordinates.length - 1, 196 | x = 0, 197 | y = 0, 198 | a, 199 | b, 200 | c; 201 | if (!arguments.length) k = -1 / (6 * coordinates.area()); 202 | while (++i < n) { 203 | a = coordinates[i]; 204 | b = coordinates[i + 1]; 205 | c = a[0] * b[1] - b[0] * a[1]; 206 | x += (a[0] + b[0]) * c; 207 | y += (a[1] + b[1]) * c; 208 | } 209 | return [x * k, y * k]; 210 | }; 211 | 212 | // The Sutherland-Hodgman clipping algorithm. 213 | coordinates.clip = function(subject) { 214 | var input, 215 | i = -1, 216 | n = coordinates.length, 217 | j, 218 | m, 219 | a = coordinates[n - 1], 220 | b, 221 | c, 222 | d; 223 | while (++i < n) { 224 | input = subject.slice(); 225 | subject.length = 0; 226 | b = coordinates[i]; 227 | c = input[(m = input.length) - 1]; 228 | j = -1; 229 | while (++j < m) { 230 | d = input[j]; 231 | if (d3_geom_polygonInside(d, a, b)) { 232 | if (!d3_geom_polygonInside(c, a, b)) { 233 | subject.push(d3_geom_polygonIntersect(c, d, a, b)); 234 | } 235 | subject.push(d); 236 | } else if (d3_geom_polygonInside(c, a, b)) { 237 | subject.push(d3_geom_polygonIntersect(c, d, a, b)); 238 | } 239 | c = d; 240 | } 241 | a = b; 242 | } 243 | return subject; 244 | }; 245 | 246 | return coordinates; 247 | }; 248 | 249 | function d3_geom_polygonInside(p, a, b) { 250 | return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]); 251 | } 252 | 253 | // Intersect two infinite lines cd and ab. 254 | function d3_geom_polygonIntersect(c, d, a, b) { 255 | var x1 = c[0], x2 = d[0], x3 = a[0], x4 = b[0], 256 | y1 = c[1], y2 = d[1], y3 = a[1], y4 = b[1], 257 | x13 = x1 - x3, 258 | x21 = x2 - x1, 259 | x43 = x4 - x3, 260 | y13 = y1 - y3, 261 | y21 = y2 - y1, 262 | y43 = y4 - y3, 263 | ua = (x43 * y13 - y43 * x13) / (y43 * x21 - x43 * y21); 264 | return [x1 + ua * x21, y1 + ua * y21]; 265 | } 266 | // Adapted from Nicolas Garcia Belmonte's JIT implementation: 267 | // http://blog.thejit.org/2010/02/12/voronoi-tessellation/ 268 | // http://blog.thejit.org/assets/voronoijs/voronoi.js 269 | // See lib/jit/LICENSE for details. 270 | 271 | /** 272 | * @param vertices [[x1, y1], [x2, y2], …] 273 | * @returns polygons [[[x1, y1], [x2, y2], …], …] 274 | */ 275 | d3.geom.voronoi = function(vertices) { 276 | var polygons = vertices.map(function() { return []; }); 277 | 278 | // Note: we expect the caller to clip the polygons, if needed. 279 | d3_voronoi_tessellate(vertices, function(e) { 280 | var s1, 281 | s2, 282 | x1, 283 | x2, 284 | y1, 285 | y2; 286 | if (e.a === 1 && e.b >= 0) { 287 | s1 = e.ep.r; 288 | s2 = e.ep.l; 289 | } else { 290 | s1 = e.ep.l; 291 | s2 = e.ep.r; 292 | } 293 | if (e.a === 1) { 294 | y1 = s1 ? s1.y : -1e6; 295 | x1 = e.c - e.b * y1; 296 | y2 = s2 ? s2.y : 1e6; 297 | x2 = e.c - e.b * y2; 298 | } else { 299 | x1 = s1 ? s1.x : -1e6; 300 | y1 = e.c - e.a * x1; 301 | x2 = s2 ? s2.x : 1e6; 302 | y2 = e.c - e.a * x2; 303 | } 304 | var v1 = [x1, y1], 305 | v2 = [x2, y2]; 306 | polygons[e.region.l.index].push(v1, v2); 307 | polygons[e.region.r.index].push(v1, v2); 308 | }); 309 | 310 | // Reconnect the polygon segments into counterclockwise loops. 311 | return polygons.map(function(polygon, i) { 312 | var cx = vertices[i][0], 313 | cy = vertices[i][1]; 314 | polygon.forEach(function(v) { 315 | v.angle = Math.atan2(v[0] - cx, v[1] - cy); 316 | }); 317 | return polygon.sort(function(a, b) { 318 | return a.angle - b.angle; 319 | }).filter(function(d, i) { 320 | return !i || (d.angle - polygon[i - 1].angle > 1e-10); 321 | }); 322 | }); 323 | }; 324 | 325 | var d3_voronoi_opposite = {"l": "r", "r": "l"}; 326 | 327 | function d3_voronoi_tessellate(vertices, callback) { 328 | 329 | var Sites = { 330 | list: vertices 331 | .map(function(v, i) { 332 | return { 333 | index: i, 334 | x: v[0], 335 | y: v[1] 336 | }; 337 | }) 338 | .sort(function(a, b) { 339 | return a.y < b.y ? -1 340 | : a.y > b.y ? 1 341 | : a.x < b.x ? -1 342 | : a.x > b.x ? 1 343 | : 0; 344 | }), 345 | bottomSite: null 346 | }; 347 | 348 | var EdgeList = { 349 | list: [], 350 | leftEnd: null, 351 | rightEnd: null, 352 | 353 | init: function() { 354 | EdgeList.leftEnd = EdgeList.createHalfEdge(null, "l"); 355 | EdgeList.rightEnd = EdgeList.createHalfEdge(null, "l"); 356 | EdgeList.leftEnd.r = EdgeList.rightEnd; 357 | EdgeList.rightEnd.l = EdgeList.leftEnd; 358 | EdgeList.list.unshift(EdgeList.leftEnd, EdgeList.rightEnd); 359 | }, 360 | 361 | createHalfEdge: function(edge, side) { 362 | return { 363 | edge: edge, 364 | side: side, 365 | vertex: null, 366 | "l": null, 367 | "r": null 368 | }; 369 | }, 370 | 371 | insert: function(lb, he) { 372 | he.l = lb; 373 | he.r = lb.r; 374 | lb.r.l = he; 375 | lb.r = he; 376 | }, 377 | 378 | leftBound: function(p) { 379 | var he = EdgeList.leftEnd; 380 | do { 381 | he = he.r; 382 | } while (he != EdgeList.rightEnd && Geom.rightOf(he, p)); 383 | he = he.l; 384 | return he; 385 | }, 386 | 387 | del: function(he) { 388 | he.l.r = he.r; 389 | he.r.l = he.l; 390 | he.edge = null; 391 | }, 392 | 393 | right: function(he) { 394 | return he.r; 395 | }, 396 | 397 | left: function(he) { 398 | return he.l; 399 | }, 400 | 401 | leftRegion: function(he) { 402 | return he.edge == null 403 | ? Sites.bottomSite 404 | : he.edge.region[he.side]; 405 | }, 406 | 407 | rightRegion: function(he) { 408 | return he.edge == null 409 | ? Sites.bottomSite 410 | : he.edge.region[d3_voronoi_opposite[he.side]]; 411 | } 412 | }; 413 | 414 | var Geom = { 415 | 416 | bisect: function(s1, s2) { 417 | var newEdge = { 418 | region: {"l": s1, "r": s2}, 419 | ep: {"l": null, "r": null} 420 | }; 421 | 422 | var dx = s2.x - s1.x, 423 | dy = s2.y - s1.y, 424 | adx = dx > 0 ? dx : -dx, 425 | ady = dy > 0 ? dy : -dy; 426 | 427 | newEdge.c = s1.x * dx + s1.y * dy 428 | + (dx * dx + dy * dy) * .5; 429 | 430 | if (adx > ady) { 431 | newEdge.a = 1; 432 | newEdge.b = dy / dx; 433 | newEdge.c /= dx; 434 | } else { 435 | newEdge.b = 1; 436 | newEdge.a = dx / dy; 437 | newEdge.c /= dy; 438 | } 439 | 440 | return newEdge; 441 | }, 442 | 443 | intersect: function(el1, el2) { 444 | var e1 = el1.edge, 445 | e2 = el2.edge; 446 | if (!e1 || !e2 || (e1.region.r == e2.region.r)) { 447 | return null; 448 | } 449 | var d = (e1.a * e2.b) - (e1.b * e2.a); 450 | if (Math.abs(d) < 1e-10) { 451 | return null; 452 | } 453 | var xint = (e1.c * e2.b - e2.c * e1.b) / d, 454 | yint = (e2.c * e1.a - e1.c * e2.a) / d, 455 | e1r = e1.region.r, 456 | e2r = e2.region.r, 457 | el, 458 | e; 459 | if ((e1r.y < e2r.y) || 460 | (e1r.y == e2r.y && e1r.x < e2r.x)) { 461 | el = el1; 462 | e = e1; 463 | } else { 464 | el = el2; 465 | e = e2; 466 | } 467 | var rightOfSite = (xint >= e.region.r.x); 468 | if ((rightOfSite && (el.side === "l")) || 469 | (!rightOfSite && (el.side === "r"))) { 470 | return null; 471 | } 472 | return { 473 | x: xint, 474 | y: yint 475 | }; 476 | }, 477 | 478 | rightOf: function(he, p) { 479 | var e = he.edge, 480 | topsite = e.region.r, 481 | rightOfSite = (p.x > topsite.x); 482 | 483 | if (rightOfSite && (he.side === "l")) { 484 | return 1; 485 | } 486 | if (!rightOfSite && (he.side === "r")) { 487 | return 0; 488 | } 489 | if (e.a === 1) { 490 | var dyp = p.y - topsite.y, 491 | dxp = p.x - topsite.x, 492 | fast = 0, 493 | above = 0; 494 | 495 | if ((!rightOfSite && (e.b < 0)) || 496 | (rightOfSite && (e.b >= 0))) { 497 | above = fast = (dyp >= e.b * dxp); 498 | } else { 499 | above = ((p.x + p.y * e.b) > e.c); 500 | if (e.b < 0) { 501 | above = !above; 502 | } 503 | if (!above) { 504 | fast = 1; 505 | } 506 | } 507 | if (!fast) { 508 | var dxs = topsite.x - e.region.l.x; 509 | above = (e.b * (dxp * dxp - dyp * dyp)) < 510 | (dxs * dyp * (1 + 2 * dxp / dxs + e.b * e.b)); 511 | 512 | if (e.b < 0) { 513 | above = !above; 514 | } 515 | } 516 | } else /* e.b == 1 */ { 517 | var yl = e.c - e.a * p.x, 518 | t1 = p.y - yl, 519 | t2 = p.x - topsite.x, 520 | t3 = yl - topsite.y; 521 | 522 | above = (t1 * t1) > (t2 * t2 + t3 * t3); 523 | } 524 | return he.side === "l" ? above : !above; 525 | }, 526 | 527 | endPoint: function(edge, side, site) { 528 | edge.ep[side] = site; 529 | if (!edge.ep[d3_voronoi_opposite[side]]) return; 530 | callback(edge); 531 | }, 532 | 533 | distance: function(s, t) { 534 | var dx = s.x - t.x, 535 | dy = s.y - t.y; 536 | return Math.sqrt(dx * dx + dy * dy); 537 | } 538 | }; 539 | 540 | var EventQueue = { 541 | list: [], 542 | 543 | insert: function(he, site, offset) { 544 | he.vertex = site; 545 | he.ystar = site.y + offset; 546 | for (var i=0, list=EventQueue.list, l=list.length; i next.ystar || 549 | (he.ystar == next.ystar && 550 | site.x > next.vertex.x)) { 551 | continue; 552 | } else { 553 | break; 554 | } 555 | } 556 | list.splice(i, 0, he); 557 | }, 558 | 559 | del: function(he) { 560 | for (var i=0, ls=EventQueue.list, l=ls.length; i top.y) { 636 | temp = bot; 637 | bot = top; 638 | top = temp; 639 | pm = "r"; 640 | } 641 | e = Geom.bisect(bot, top); 642 | bisector = EdgeList.createHalfEdge(e, pm); 643 | EdgeList.insert(llbnd, bisector); 644 | Geom.endPoint(e, d3_voronoi_opposite[pm], v); 645 | p = Geom.intersect(llbnd, bisector); 646 | if (p) { 647 | EventQueue.del(llbnd); 648 | EventQueue.insert(llbnd, p, Geom.distance(p, bot)); 649 | } 650 | p = Geom.intersect(bisector, rrbnd); 651 | if (p) { 652 | EventQueue.insert(bisector, p, Geom.distance(p, bot)); 653 | } 654 | } else { 655 | break; 656 | } 657 | }//end while 658 | 659 | for (lbnd = EdgeList.right(EdgeList.leftEnd); 660 | lbnd != EdgeList.rightEnd; 661 | lbnd = EdgeList.right(lbnd)) { 662 | callback(lbnd.edge); 663 | } 664 | } 665 | /** 666 | * @param vertices [[x1, y1], [x2, y2], …] 667 | * @returns triangles [[[x1, y1], [x2, y2], [x3, y3]], …] 668 | */ 669 | d3.geom.delaunay = function(vertices) { 670 | var edges = vertices.map(function() { return []; }), 671 | triangles = []; 672 | 673 | // Use the Voronoi tessellation to determine Delaunay edges. 674 | d3_voronoi_tessellate(vertices, function(e) { 675 | edges[e.region.l.index].push(vertices[e.region.r.index]); 676 | }); 677 | 678 | // Reconnect the edges into counterclockwise triangles. 679 | edges.forEach(function(edge, i) { 680 | var v = vertices[i], 681 | cx = v[0], 682 | cy = v[1]; 683 | edge.forEach(function(v) { 684 | v.angle = Math.atan2(v[0] - cx, v[1] - cy); 685 | }); 686 | edge.sort(function(a, b) { 687 | return a.angle - b.angle; 688 | }); 689 | for (var j = 0, m = edge.length - 1; j < m; j++) { 690 | triangles.push([v, edge[j], edge[j + 1]]); 691 | } 692 | }); 693 | 694 | return triangles; 695 | }; 696 | // Constructs a new quadtree for the specified array of points. A quadtree is a 697 | // two-dimensional recursive spatial subdivision. This implementation uses 698 | // square partitions, dividing each square into four equally-sized squares. Each 699 | // point exists in a unique node; if multiple points are in the same position, 700 | // some points may be stored on internal nodes rather than leaf nodes. Quadtrees 701 | // can be used to accelerate various spatial operations, such as the Barnes-Hut 702 | // approximation for computing n-body forces, or collision detection. 703 | d3.geom.quadtree = function(points, x1, y1, x2, y2) { 704 | var p, 705 | i = -1, 706 | n = points.length; 707 | 708 | // Type conversion for deprecated API. 709 | if (n && isNaN(points[0].x)) points = points.map(d3_geom_quadtreePoint); 710 | 711 | // Allow bounds to be specified explicitly. 712 | if (arguments.length < 5) { 713 | if (arguments.length === 3) { 714 | y2 = x2 = y1; 715 | y1 = x1; 716 | } else { 717 | x1 = y1 = Infinity; 718 | x2 = y2 = -Infinity; 719 | 720 | // Compute bounds. 721 | while (++i < n) { 722 | p = points[i]; 723 | if (p.x < x1) x1 = p.x; 724 | if (p.y < y1) y1 = p.y; 725 | if (p.x > x2) x2 = p.x; 726 | if (p.y > y2) y2 = p.y; 727 | } 728 | 729 | // Squarify the bounds. 730 | var dx = x2 - x1, 731 | dy = y2 - y1; 732 | if (dx > dy) y2 = y1 + dx; 733 | else x2 = x1 + dy; 734 | } 735 | } 736 | 737 | // Recursively inserts the specified point p at the node n or one of its 738 | // descendants. The bounds are defined by [x1, x2] and [y1, y2]. 739 | function insert(n, p, x1, y1, x2, y2) { 740 | if (isNaN(p.x) || isNaN(p.y)) return; // ignore invalid points 741 | if (n.leaf) { 742 | var v = n.point; 743 | if (v) { 744 | // If the point at this leaf node is at the same position as the new 745 | // point we are adding, we leave the point associated with the 746 | // internal node while adding the new point to a child node. This 747 | // avoids infinite recursion. 748 | if ((Math.abs(v.x - p.x) + Math.abs(v.y - p.y)) < .01) { 749 | insertChild(n, p, x1, y1, x2, y2); 750 | } else { 751 | n.point = null; 752 | insertChild(n, v, x1, y1, x2, y2); 753 | insertChild(n, p, x1, y1, x2, y2); 754 | } 755 | } else { 756 | n.point = p; 757 | } 758 | } else { 759 | insertChild(n, p, x1, y1, x2, y2); 760 | } 761 | } 762 | 763 | // Recursively inserts the specified point p into a descendant of node n. The 764 | // bounds are defined by [x1, x2] and [y1, y2]. 765 | function insertChild(n, p, x1, y1, x2, y2) { 766 | // Compute the split point, and the quadrant in which to insert p. 767 | var sx = (x1 + x2) * .5, 768 | sy = (y1 + y2) * .5, 769 | right = p.x >= sx, 770 | bottom = p.y >= sy, 771 | i = (bottom << 1) + right; 772 | 773 | // Recursively insert into the child node. 774 | n.leaf = false; 775 | n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode()); 776 | 777 | // Update the bounds as we recurse. 778 | if (right) x1 = sx; else x2 = sx; 779 | if (bottom) y1 = sy; else y2 = sy; 780 | insert(n, p, x1, y1, x2, y2); 781 | } 782 | 783 | // Create the root node. 784 | var root = d3_geom_quadtreeNode(); 785 | 786 | root.add = function(p) { 787 | insert(root, p, x1, y1, x2, y2); 788 | }; 789 | 790 | root.visit = function(f) { 791 | d3_geom_quadtreeVisit(f, root, x1, y1, x2, y2); 792 | }; 793 | 794 | // Insert all points. 795 | points.forEach(root.add); 796 | return root; 797 | }; 798 | 799 | function d3_geom_quadtreeNode() { 800 | return { 801 | leaf: true, 802 | nodes: [], 803 | point: null 804 | }; 805 | } 806 | 807 | function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) { 808 | if (!f(node, x1, y1, x2, y2)) { 809 | var sx = (x1 + x2) * .5, 810 | sy = (y1 + y2) * .5, 811 | children = node.nodes; 812 | if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy); 813 | if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy); 814 | if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2); 815 | if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2); 816 | } 817 | } 818 | 819 | function d3_geom_quadtreePoint(p) { 820 | return { 821 | x: p[0], 822 | y: p[1] 823 | }; 824 | } 825 | })(); 826 | -------------------------------------------------------------------------------- /app/assets/javascripts/d3.layout.js: -------------------------------------------------------------------------------- 1 | (function(){d3.layout = {}; 2 | // Implements hierarchical edge bundling using Holten's algorithm. For each 3 | // input link, a path is computed that travels through the tree, up the parent 4 | // hierarchy to the least common ancestor, and then back down to the destination 5 | // node. Each path is simply an array of nodes. 6 | d3.layout.bundle = function() { 7 | return function(links) { 8 | var paths = [], 9 | i = -1, 10 | n = links.length; 11 | while (++i < n) paths.push(d3_layout_bundlePath(links[i])); 12 | return paths; 13 | }; 14 | }; 15 | 16 | function d3_layout_bundlePath(link) { 17 | var start = link.source, 18 | end = link.target, 19 | lca = d3_layout_bundleLeastCommonAncestor(start, end), 20 | points = [start]; 21 | while (start !== lca) { 22 | start = start.parent; 23 | points.push(start); 24 | } 25 | var k = points.length; 26 | while (end !== lca) { 27 | points.splice(k, 0, end); 28 | end = end.parent; 29 | } 30 | return points; 31 | } 32 | 33 | function d3_layout_bundleAncestors(node) { 34 | var ancestors = [], 35 | parent = node.parent; 36 | while (parent != null) { 37 | ancestors.push(node); 38 | node = parent; 39 | parent = parent.parent; 40 | } 41 | ancestors.push(node); 42 | return ancestors; 43 | } 44 | 45 | function d3_layout_bundleLeastCommonAncestor(a, b) { 46 | if (a === b) return a; 47 | var aNodes = d3_layout_bundleAncestors(a), 48 | bNodes = d3_layout_bundleAncestors(b), 49 | aNode = aNodes.pop(), 50 | bNode = bNodes.pop(), 51 | sharedNode = null; 52 | while (aNode === bNode) { 53 | sharedNode = aNode; 54 | aNode = aNodes.pop(); 55 | bNode = bNodes.pop(); 56 | } 57 | return sharedNode; 58 | } 59 | d3.layout.chord = function() { 60 | var chord = {}, 61 | chords, 62 | groups, 63 | matrix, 64 | n, 65 | padding = 0, 66 | sortGroups, 67 | sortSubgroups, 68 | sortChords; 69 | 70 | function relayout() { 71 | var subgroups = {}, 72 | groupSums = [], 73 | groupIndex = d3.range(n), 74 | subgroupIndex = [], 75 | k, 76 | x, 77 | x0, 78 | i, 79 | j; 80 | 81 | chords = []; 82 | groups = []; 83 | 84 | // Compute the sum. 85 | k = 0, i = -1; while (++i < n) { 86 | x = 0, j = -1; while (++j < n) { 87 | x += matrix[i][j]; 88 | } 89 | groupSums.push(x); 90 | subgroupIndex.push(d3.range(n)); 91 | k += x; 92 | } 93 | 94 | // Sort groups… 95 | if (sortGroups) { 96 | groupIndex.sort(function(a, b) { 97 | return sortGroups(groupSums[a], groupSums[b]); 98 | }); 99 | } 100 | 101 | // Sort subgroups… 102 | if (sortSubgroups) { 103 | subgroupIndex.forEach(function(d, i) { 104 | d.sort(function(a, b) { 105 | return sortSubgroups(matrix[i][a], matrix[i][b]); 106 | }); 107 | }); 108 | } 109 | 110 | // Convert the sum to scaling factor for [0, 2pi]. 111 | // TODO Allow start and end angle to be specified. 112 | // TODO Allow padding to be specified as percentage? 113 | k = (2 * Math.PI - padding * n) / k; 114 | 115 | // Compute the start and end angle for each group and subgroup. 116 | x = 0, i = -1; while (++i < n) { 117 | x0 = x, j = -1; while (++j < n) { 118 | var di = groupIndex[i], 119 | dj = subgroupIndex[di][j], 120 | v = matrix[di][dj]; 121 | subgroups[di + "-" + dj] = { 122 | index: di, 123 | subindex: dj, 124 | startAngle: x, 125 | endAngle: x += v * k, 126 | value: v 127 | }; 128 | } 129 | groups.push({ 130 | index: di, 131 | startAngle: x0, 132 | endAngle: x, 133 | value: (x - x0) / k 134 | }); 135 | x += padding; 136 | } 137 | 138 | // Generate chords for each (non-empty) subgroup-subgroup link. 139 | i = -1; while (++i < n) { 140 | j = i - 1; while (++j < n) { 141 | var source = subgroups[i + "-" + j], 142 | target = subgroups[j + "-" + i]; 143 | if (source.value || target.value) { 144 | chords.push(source.value < target.value 145 | ? {source: target, target: source} 146 | : {source: source, target: target}); 147 | } 148 | } 149 | } 150 | 151 | if (sortChords) resort(); 152 | } 153 | 154 | function resort() { 155 | chords.sort(function(a, b) { 156 | return sortChords( 157 | (a.source.value + a.target.value) / 2, 158 | (b.source.value + b.target.value) / 2); 159 | }); 160 | } 161 | 162 | chord.matrix = function(x) { 163 | if (!arguments.length) return matrix; 164 | n = (matrix = x) && matrix.length; 165 | chords = groups = null; 166 | return chord; 167 | }; 168 | 169 | chord.padding = function(x) { 170 | if (!arguments.length) return padding; 171 | padding = x; 172 | chords = groups = null; 173 | return chord; 174 | }; 175 | 176 | chord.sortGroups = function(x) { 177 | if (!arguments.length) return sortGroups; 178 | sortGroups = x; 179 | chords = groups = null; 180 | return chord; 181 | }; 182 | 183 | chord.sortSubgroups = function(x) { 184 | if (!arguments.length) return sortSubgroups; 185 | sortSubgroups = x; 186 | chords = null; 187 | return chord; 188 | }; 189 | 190 | chord.sortChords = function(x) { 191 | if (!arguments.length) return sortChords; 192 | sortChords = x; 193 | if (chords) resort(); 194 | return chord; 195 | }; 196 | 197 | chord.chords = function() { 198 | if (!chords) relayout(); 199 | return chords; 200 | }; 201 | 202 | chord.groups = function() { 203 | if (!groups) relayout(); 204 | return groups; 205 | }; 206 | 207 | return chord; 208 | }; 209 | // A rudimentary force layout using Gauss-Seidel. 210 | d3.layout.force = function() { 211 | var force = {}, 212 | event = d3.dispatch("tick"), 213 | size = [1, 1], 214 | drag, 215 | alpha, 216 | friction = .9, 217 | linkDistance = d3_layout_forceLinkDistance, 218 | linkStrength = d3_layout_forceLinkStrength, 219 | charge = -30, 220 | gravity = .1, 221 | theta = .8, 222 | interval, 223 | nodes = [], 224 | links = [], 225 | distances, 226 | strengths, 227 | charges; 228 | 229 | function repulse(node) { 230 | return function(quad, x1, y1, x2, y2) { 231 | if (quad.point !== node) { 232 | var dx = quad.cx - node.x, 233 | dy = quad.cy - node.y, 234 | dn = 1 / Math.sqrt(dx * dx + dy * dy); 235 | 236 | /* Barnes-Hut criterion. */ 237 | if ((x2 - x1) * dn < theta) { 238 | var k = quad.charge * dn * dn; 239 | node.px -= dx * k; 240 | node.py -= dy * k; 241 | return true; 242 | } 243 | 244 | if (quad.point && isFinite(dn)) { 245 | var k = quad.pointCharge * dn * dn; 246 | node.px -= dx * k; 247 | node.py -= dy * k; 248 | } 249 | } 250 | return !quad.charge; 251 | }; 252 | } 253 | 254 | function tick() { 255 | var n = nodes.length, 256 | m = links.length, 257 | q, 258 | i, // current index 259 | o, // current object 260 | s, // current source 261 | t, // current target 262 | l, // current distance 263 | k, // current force 264 | x, // x-distance 265 | y; // y-distance 266 | 267 | // gauss-seidel relaxation for links 268 | for (i = 0; i < m; ++i) { 269 | o = links[i]; 270 | s = o.source; 271 | t = o.target; 272 | x = t.x - s.x; 273 | y = t.y - s.y; 274 | if (l = (x * x + y * y)) { 275 | l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; 276 | x *= l; 277 | y *= l; 278 | t.x -= x * (k = s.weight / (t.weight + s.weight)); 279 | t.y -= y * k; 280 | s.x += x * (k = 1 - k); 281 | s.y += y * k; 282 | } 283 | } 284 | 285 | // apply gravity forces 286 | if (k = alpha * gravity) { 287 | x = size[0] / 2; 288 | y = size[1] / 2; 289 | i = -1; if (k) while (++i < n) { 290 | o = nodes[i]; 291 | o.x += (x - o.x) * k; 292 | o.y += (y - o.y) * k; 293 | } 294 | } 295 | 296 | // compute quadtree center of mass and apply charge forces 297 | if (charge) { 298 | d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); 299 | i = -1; while (++i < n) { 300 | if (!(o = nodes[i]).fixed) { 301 | q.visit(repulse(o)); 302 | } 303 | } 304 | } 305 | 306 | // position verlet integration 307 | i = -1; while (++i < n) { 308 | o = nodes[i]; 309 | if (o.fixed) { 310 | o.x = o.px; 311 | o.y = o.py; 312 | } else { 313 | o.x -= (o.px - (o.px = o.x)) * friction; 314 | o.y -= (o.py - (o.py = o.y)) * friction; 315 | } 316 | } 317 | 318 | event.tick.dispatch({type: "tick", alpha: alpha}); 319 | 320 | // simulated annealing, basically 321 | return (alpha *= .99) < .005; 322 | } 323 | 324 | force.on = function(type, listener) { 325 | event[type].add(listener); 326 | return force; 327 | }; 328 | 329 | force.nodes = function(x) { 330 | if (!arguments.length) return nodes; 331 | nodes = x; 332 | return force; 333 | }; 334 | 335 | force.links = function(x) { 336 | if (!arguments.length) return links; 337 | links = x; 338 | return force; 339 | }; 340 | 341 | force.size = function(x) { 342 | if (!arguments.length) return size; 343 | size = x; 344 | return force; 345 | }; 346 | 347 | force.linkDistance = function(x) { 348 | if (!arguments.length) return linkDistance; 349 | linkDistance = d3.functor(x); 350 | return force; 351 | }; 352 | 353 | // For backwards-compatibility. 354 | force.distance = force.linkDistance; 355 | 356 | force.linkStrength = function(x) { 357 | if (!arguments.length) return linkStrength; 358 | linkStrength = d3.functor(x); 359 | return force; 360 | }; 361 | 362 | force.friction = function(x) { 363 | if (!arguments.length) return friction; 364 | friction = x; 365 | return force; 366 | }; 367 | 368 | force.charge = function(x) { 369 | if (!arguments.length) return charge; 370 | charge = typeof x === "function" ? x : +x; 371 | return force; 372 | }; 373 | 374 | force.gravity = function(x) { 375 | if (!arguments.length) return gravity; 376 | gravity = x; 377 | return force; 378 | }; 379 | 380 | force.theta = function(x) { 381 | if (!arguments.length) return theta; 382 | theta = x; 383 | return force; 384 | }; 385 | 386 | force.start = function() { 387 | var i, 388 | j, 389 | n = nodes.length, 390 | m = links.length, 391 | w = size[0], 392 | h = size[1], 393 | neighbors, 394 | o; 395 | 396 | for (i = 0; i < n; ++i) { 397 | (o = nodes[i]).index = i; 398 | o.weight = 0; 399 | } 400 | 401 | distances = []; 402 | strengths = []; 403 | for (i = 0; i < m; ++i) { 404 | o = links[i]; 405 | if (typeof o.source == "number") o.source = nodes[o.source]; 406 | if (typeof o.target == "number") o.target = nodes[o.target]; 407 | distances[i] = linkDistance.call(this, o, i); 408 | strengths[i] = linkStrength.call(this, o, i); 409 | ++o.source.weight; 410 | ++o.target.weight; 411 | } 412 | 413 | for (i = 0; i < n; ++i) { 414 | o = nodes[i]; 415 | if (isNaN(o.x)) o.x = position("x", w); 416 | if (isNaN(o.y)) o.y = position("y", h); 417 | if (isNaN(o.px)) o.px = o.x; 418 | if (isNaN(o.py)) o.py = o.y; 419 | } 420 | 421 | charges = []; 422 | if (typeof charge === "function") { 423 | for (i = 0; i < n; ++i) { 424 | charges[i] = +charge.call(this, nodes[i], i); 425 | } 426 | } else { 427 | for (i = 0; i < n; ++i) { 428 | charges[i] = charge; 429 | } 430 | } 431 | 432 | // initialize node position based on first neighbor 433 | function position(dimension, size) { 434 | var neighbors = neighbor(i), 435 | j = -1, 436 | m = neighbors.length, 437 | x; 438 | while (++j < m) if (!isNaN(x = neighbors[j][dimension])) return x; 439 | return Math.random() * size; 440 | } 441 | 442 | // initialize neighbors lazily 443 | function neighbor() { 444 | if (!neighbors) { 445 | neighbors = []; 446 | for (j = 0; j < n; ++j) { 447 | neighbors[j] = []; 448 | } 449 | for (j = 0; j < m; ++j) { 450 | var o = links[j]; 451 | neighbors[o.source.index].push(o.target); 452 | neighbors[o.target.index].push(o.source); 453 | } 454 | } 455 | return neighbors[i]; 456 | } 457 | 458 | return force.resume(); 459 | }; 460 | 461 | force.resume = function() { 462 | alpha = .1; 463 | d3.timer(tick); 464 | return force; 465 | }; 466 | 467 | force.stop = function() { 468 | alpha = 0; 469 | return force; 470 | }; 471 | 472 | // use `node.call(force.drag)` to make nodes draggable 473 | force.drag = function() { 474 | if (!drag) drag = d3.behavior.drag() 475 | .on("dragstart", dragstart) 476 | .on("drag", d3_layout_forceDrag) 477 | .on("dragend", d3_layout_forceDragEnd); 478 | 479 | this.on("mouseover.force", d3_layout_forceDragOver) 480 | .on("mouseout.force", d3_layout_forceDragOut) 481 | .call(drag); 482 | }; 483 | 484 | function dragstart(d) { 485 | d3_layout_forceDragOver(d3_layout_forceDragNode = d); 486 | d3_layout_forceDragForce = force; 487 | } 488 | 489 | return force; 490 | }; 491 | 492 | var d3_layout_forceDragForce, 493 | d3_layout_forceDragNode; 494 | 495 | function d3_layout_forceDragOver(d) { 496 | d.fixed |= 2; 497 | } 498 | 499 | function d3_layout_forceDragOut(d) { 500 | if (d !== d3_layout_forceDragNode) d.fixed &= 1; 501 | } 502 | 503 | function d3_layout_forceDragEnd() { 504 | d3_layout_forceDrag(); 505 | d3_layout_forceDragNode.fixed &= 1; 506 | d3_layout_forceDragForce = d3_layout_forceDragNode = null; 507 | } 508 | 509 | function d3_layout_forceDrag() { 510 | d3_layout_forceDragNode.px += d3.event.dx; 511 | d3_layout_forceDragNode.py += d3.event.dy; 512 | d3_layout_forceDragForce.resume(); // restart annealing 513 | } 514 | 515 | function d3_layout_forceAccumulate(quad, alpha, charges) { 516 | var cx = 0, 517 | cy = 0; 518 | quad.charge = 0; 519 | if (!quad.leaf) { 520 | var nodes = quad.nodes, 521 | n = nodes.length, 522 | i = -1, 523 | c; 524 | while (++i < n) { 525 | c = nodes[i]; 526 | if (c == null) continue; 527 | d3_layout_forceAccumulate(c, alpha, charges); 528 | quad.charge += c.charge; 529 | cx += c.charge * c.cx; 530 | cy += c.charge * c.cy; 531 | } 532 | } 533 | if (quad.point) { 534 | // jitter internal nodes that are coincident 535 | if (!quad.leaf) { 536 | quad.point.x += Math.random() - .5; 537 | quad.point.y += Math.random() - .5; 538 | } 539 | var k = alpha * charges[quad.point.index]; 540 | quad.charge += quad.pointCharge = k; 541 | cx += k * quad.point.x; 542 | cy += k * quad.point.y; 543 | } 544 | quad.cx = cx / quad.charge; 545 | quad.cy = cy / quad.charge; 546 | } 547 | 548 | function d3_layout_forceLinkDistance(link) { 549 | return 20; 550 | } 551 | 552 | function d3_layout_forceLinkStrength(link) { 553 | return 1; 554 | } 555 | d3.layout.partition = function() { 556 | var hierarchy = d3.layout.hierarchy(), 557 | size = [1, 1]; // width, height 558 | 559 | function position(node, x, dx, dy) { 560 | var children = node.children; 561 | node.x = x; 562 | node.y = node.depth * dy; 563 | node.dx = dx; 564 | node.dy = dy; 565 | if (children && (n = children.length)) { 566 | var i = -1, 567 | n, 568 | c, 569 | d; 570 | dx = node.value ? dx / node.value : 0; 571 | while (++i < n) { 572 | position(c = children[i], x, d = c.value * dx, dy); 573 | x += d; 574 | } 575 | } 576 | } 577 | 578 | function depth(node) { 579 | var children = node.children, 580 | d = 0; 581 | if (children && (n = children.length)) { 582 | var i = -1, 583 | n; 584 | while (++i < n) d = Math.max(d, depth(children[i])); 585 | } 586 | return 1 + d; 587 | } 588 | 589 | function partition(d, i) { 590 | var nodes = hierarchy.call(this, d, i); 591 | position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); 592 | return nodes; 593 | } 594 | 595 | partition.size = function(x) { 596 | if (!arguments.length) return size; 597 | size = x; 598 | return partition; 599 | }; 600 | 601 | return d3_layout_hierarchyRebind(partition, hierarchy); 602 | }; 603 | d3.layout.pie = function() { 604 | var value = Number, 605 | sort = null, 606 | startAngle = 0, 607 | endAngle = 2 * Math.PI; 608 | 609 | function pie(data, i) { 610 | 611 | // Compute the start angle. 612 | var a = +(typeof startAngle === "function" 613 | ? startAngle.apply(this, arguments) 614 | : startAngle); 615 | 616 | // Compute the angular range (end - start). 617 | var k = (typeof endAngle === "function" 618 | ? endAngle.apply(this, arguments) 619 | : endAngle) - startAngle; 620 | 621 | // Optionally sort the data. 622 | var index = d3.range(data.length); 623 | if (sort != null) index.sort(function(i, j) { 624 | return sort(data[i], data[j]); 625 | }); 626 | 627 | // Compute the numeric values for each data element. 628 | var values = data.map(value); 629 | 630 | // Convert k into a scale factor from value to angle, using the sum. 631 | k /= values.reduce(function(p, d) { return p + d; }, 0); 632 | 633 | // Compute the arcs! 634 | var arcs = index.map(function(i) { 635 | return { 636 | data: data[i], 637 | value: d = values[i], 638 | startAngle: a, 639 | endAngle: a += d * k 640 | }; 641 | }); 642 | 643 | // Return the arcs in the original data's order. 644 | return data.map(function(d, i) { 645 | return arcs[index[i]]; 646 | }); 647 | } 648 | 649 | /** 650 | * Specifies the value function *x*, which returns a nonnegative numeric value 651 | * for each datum. The default value function is `Number`. The value function 652 | * is passed two arguments: the current datum and the current index. 653 | */ 654 | pie.value = function(x) { 655 | if (!arguments.length) return value; 656 | value = x; 657 | return pie; 658 | }; 659 | 660 | /** 661 | * Specifies a sort comparison operator *x*. The comparator is passed two data 662 | * elements from the data array, a and b; it returns a negative value if a is 663 | * less than b, a positive value if a is greater than b, and zero if a equals 664 | * b. 665 | */ 666 | pie.sort = function(x) { 667 | if (!arguments.length) return sort; 668 | sort = x; 669 | return pie; 670 | }; 671 | 672 | /** 673 | * Specifies the overall start angle of the pie chart. Defaults to 0. The 674 | * start angle can be specified either as a constant or as a function; in the 675 | * case of a function, it is evaluated once per array (as opposed to per 676 | * element). 677 | */ 678 | pie.startAngle = function(x) { 679 | if (!arguments.length) return startAngle; 680 | startAngle = x; 681 | return pie; 682 | }; 683 | 684 | /** 685 | * Specifies the overall end angle of the pie chart. Defaults to 2π. The 686 | * end angle can be specified either as a constant or as a function; in the 687 | * case of a function, it is evaluated once per array (as opposed to per 688 | * element). 689 | */ 690 | pie.endAngle = function(x) { 691 | if (!arguments.length) return endAngle; 692 | endAngle = x; 693 | return pie; 694 | }; 695 | 696 | return pie; 697 | }; 698 | // data is two-dimensional array of x,y; we populate y0 699 | d3.layout.stack = function() { 700 | var values = Object, 701 | order = d3_layout_stackOrders["default"], 702 | offset = d3_layout_stackOffsets["zero"], 703 | out = d3_layout_stackOut, 704 | x = d3_layout_stackX, 705 | y = d3_layout_stackY; 706 | 707 | function stack(data, index) { 708 | 709 | // Convert series to canonical two-dimensional representation. 710 | var series = data.map(function(d, i) { 711 | return values.call(stack, d, i); 712 | }); 713 | 714 | // Convert each series to canonical [[x,y]] representation. 715 | var points = series.map(function(d, i) { 716 | return d.map(function(v, i) { 717 | return [x.call(stack, v, i), y.call(stack, v, i)]; 718 | }); 719 | }); 720 | 721 | // Compute the order of series, and permute them. 722 | var orders = order.call(stack, points, index); 723 | series = d3.permute(series, orders); 724 | points = d3.permute(points, orders); 725 | 726 | // Compute the baseline… 727 | var offsets = offset.call(stack, points, index); 728 | 729 | // And propagate it to other series. 730 | var n = series.length, 731 | m = series[0].length, 732 | i, 733 | j, 734 | o; 735 | for (j = 0; j < m; ++j) { 736 | out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); 737 | for (i = 1; i < n; ++i) { 738 | out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); 739 | } 740 | } 741 | 742 | return data; 743 | } 744 | 745 | stack.values = function(x) { 746 | if (!arguments.length) return values; 747 | values = x; 748 | return stack; 749 | }; 750 | 751 | stack.order = function(x) { 752 | if (!arguments.length) return order; 753 | order = typeof x === "function" ? x : d3_layout_stackOrders[x]; 754 | return stack; 755 | }; 756 | 757 | stack.offset = function(x) { 758 | if (!arguments.length) return offset; 759 | offset = typeof x === "function" ? x : d3_layout_stackOffsets[x]; 760 | return stack; 761 | }; 762 | 763 | stack.x = function(z) { 764 | if (!arguments.length) return x; 765 | x = z; 766 | return stack; 767 | }; 768 | 769 | stack.y = function(z) { 770 | if (!arguments.length) return y; 771 | y = z; 772 | return stack; 773 | }; 774 | 775 | stack.out = function(z) { 776 | if (!arguments.length) return out; 777 | out = z; 778 | return stack; 779 | }; 780 | 781 | return stack; 782 | } 783 | 784 | function d3_layout_stackX(d) { 785 | return d.x; 786 | } 787 | 788 | function d3_layout_stackY(d) { 789 | return d.y; 790 | } 791 | 792 | function d3_layout_stackOut(d, y0, y) { 793 | d.y0 = y0; 794 | d.y = y; 795 | } 796 | 797 | var d3_layout_stackOrders = { 798 | 799 | "inside-out": function(data) { 800 | var n = data.length, 801 | i, 802 | j, 803 | max = data.map(d3_layout_stackMaxIndex), 804 | sums = data.map(d3_layout_stackReduceSum), 805 | index = d3.range(n).sort(function(a, b) { return max[a] - max[b]; }), 806 | top = 0, 807 | bottom = 0, 808 | tops = [], 809 | bottoms = []; 810 | for (i = 0; i < n; ++i) { 811 | j = index[i]; 812 | if (top < bottom) { 813 | top += sums[j]; 814 | tops.push(j); 815 | } else { 816 | bottom += sums[j]; 817 | bottoms.push(j); 818 | } 819 | } 820 | return bottoms.reverse().concat(tops); 821 | }, 822 | 823 | "reverse": function(data) { 824 | return d3.range(data.length).reverse(); 825 | }, 826 | 827 | "default": function(data) { 828 | return d3.range(data.length); 829 | } 830 | 831 | }; 832 | 833 | var d3_layout_stackOffsets = { 834 | 835 | "silhouette": function(data) { 836 | var n = data.length, 837 | m = data[0].length, 838 | sums = [], 839 | max = 0, 840 | i, 841 | j, 842 | o, 843 | y0 = []; 844 | for (j = 0; j < m; ++j) { 845 | for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; 846 | if (o > max) max = o; 847 | sums.push(o); 848 | } 849 | for (j = 0; j < m; ++j) { 850 | y0[j] = (max - sums[j]) / 2; 851 | } 852 | return y0; 853 | }, 854 | 855 | "wiggle": function(data) { 856 | var n = data.length, 857 | x = data[0], 858 | m = x.length, 859 | max = 0, 860 | i, 861 | j, 862 | k, 863 | s1, 864 | s2, 865 | s3, 866 | dx, 867 | o, 868 | o0, 869 | y0 = []; 870 | y0[0] = o = o0 = 0; 871 | for (j = 1; j < m; ++j) { 872 | for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; 873 | for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { 874 | for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { 875 | s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; 876 | } 877 | s2 += s3 * data[i][j][1]; 878 | } 879 | y0[j] = o -= s1 ? s2 / s1 * dx : 0; 880 | if (o < o0) o0 = o; 881 | } 882 | for (j = 0; j < m; ++j) y0[j] -= o0; 883 | return y0; 884 | }, 885 | 886 | "expand": function(data) { 887 | var n = data.length, 888 | m = data[0].length, 889 | k = 1 / n, 890 | i, 891 | j, 892 | o, 893 | y0 = []; 894 | for (j = 0; j < m; ++j) { 895 | for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; 896 | if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; 897 | else for (i = 0; i < n; i++) data[i][j][1] = k; 898 | } 899 | for (j = 0; j < m; ++j) y0[j] = 0; 900 | return y0; 901 | }, 902 | 903 | "zero": function(data) { 904 | var j = -1, 905 | m = data[0].length, 906 | y0 = []; 907 | while (++j < m) y0[j] = 0; 908 | return y0; 909 | } 910 | 911 | }; 912 | 913 | function d3_layout_stackMaxIndex(array) { 914 | var i = 1, 915 | j = 0, 916 | v = array[0][1], 917 | k, 918 | n = array.length; 919 | for (; i < n; ++i) { 920 | if ((k = array[i][1]) > v) { 921 | j = i; 922 | v = k; 923 | } 924 | } 925 | return j; 926 | } 927 | 928 | function d3_layout_stackReduceSum(d) { 929 | return d.reduce(d3_layout_stackSum, 0); 930 | } 931 | 932 | function d3_layout_stackSum(p, d) { 933 | return p + d[1]; 934 | } 935 | d3.layout.histogram = function() { 936 | var frequency = true, 937 | valuer = Number, 938 | ranger = d3_layout_histogramRange, 939 | binner = d3_layout_histogramBinSturges; 940 | 941 | function histogram(data, i) { 942 | var bins = [], 943 | values = data.map(valuer, this), 944 | range = ranger.call(this, values, i), 945 | thresholds = binner.call(this, range, values, i), 946 | bin, 947 | i = -1, 948 | n = values.length, 949 | m = thresholds.length - 1, 950 | k = frequency ? 1 : 1 / n, 951 | x; 952 | 953 | // Initialize the bins. 954 | while (++i < m) { 955 | bin = bins[i] = []; 956 | bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); 957 | bin.y = 0; 958 | } 959 | 960 | // Fill the bins, ignoring values outside the range. 961 | i = -1; while(++i < n) { 962 | x = values[i]; 963 | if ((x >= range[0]) && (x <= range[1])) { 964 | bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; 965 | bin.y += k; 966 | bin.push(data[i]); 967 | } 968 | } 969 | 970 | return bins; 971 | } 972 | 973 | // Specifies how to extract a value from the associated data. The default 974 | // value function is `Number`, which is equivalent to the identity function. 975 | histogram.value = function(x) { 976 | if (!arguments.length) return valuer; 977 | valuer = x; 978 | return histogram; 979 | }; 980 | 981 | // Specifies the range of the histogram. Values outside the specified range 982 | // will be ignored. The argument `x` may be specified either as a two-element 983 | // array representing the minimum and maximum value of the range, or as a 984 | // function that returns the range given the array of values and the current 985 | // index `i`. The default range is the extent (minimum and maximum) of the 986 | // values. 987 | histogram.range = function(x) { 988 | if (!arguments.length) return ranger; 989 | ranger = d3.functor(x); 990 | return histogram; 991 | }; 992 | 993 | // Specifies how to bin values in the histogram. The argument `x` may be 994 | // specified as a number, in which case the range of values will be split 995 | // uniformly into the given number of bins. Or, `x` may be an array of 996 | // threshold values, defining the bins; the specified array must contain the 997 | // rightmost (upper) value, thus specifying n + 1 values for n bins. Or, `x` 998 | // may be a function which is evaluated, being passed the range, the array of 999 | // values, and the current index `i`, returning an array of thresholds. The 1000 | // default bin function will divide the values into uniform bins using 1001 | // Sturges' formula. 1002 | histogram.bins = function(x) { 1003 | if (!arguments.length) return binner; 1004 | binner = typeof x === "number" 1005 | ? function(range) { return d3_layout_histogramBinFixed(range, x); } 1006 | : d3.functor(x); 1007 | return histogram; 1008 | }; 1009 | 1010 | // Specifies whether the histogram's `y` value is a count (frequency) or a 1011 | // probability (density). The default value is true. 1012 | histogram.frequency = function(x) { 1013 | if (!arguments.length) return frequency; 1014 | frequency = !!x; 1015 | return histogram; 1016 | }; 1017 | 1018 | return histogram; 1019 | }; 1020 | 1021 | function d3_layout_histogramBinSturges(range, values) { 1022 | return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); 1023 | } 1024 | 1025 | function d3_layout_histogramBinFixed(range, n) { 1026 | var x = -1, 1027 | b = +range[0], 1028 | m = (range[1] - b) / n, 1029 | f = []; 1030 | while (++x <= n) f[x] = m * x + b; 1031 | return f; 1032 | } 1033 | 1034 | function d3_layout_histogramRange(values) { 1035 | return [d3.min(values), d3.max(values)]; 1036 | } 1037 | d3.layout.hierarchy = function() { 1038 | var sort = d3_layout_hierarchySort, 1039 | children = d3_layout_hierarchyChildren, 1040 | value = d3_layout_hierarchyValue; 1041 | 1042 | // Recursively compute the node depth and value. 1043 | // Also converts the data representation into a standard hierarchy structure. 1044 | function recurse(data, depth, nodes) { 1045 | var childs = children.call(hierarchy, data, depth), 1046 | node = d3_layout_hierarchyInline ? data : {data: data}; 1047 | node.depth = depth; 1048 | nodes.push(node); 1049 | if (childs && (n = childs.length)) { 1050 | var i = -1, 1051 | n, 1052 | c = node.children = [], 1053 | v = 0, 1054 | j = depth + 1; 1055 | while (++i < n) { 1056 | d = recurse(childs[i], j, nodes); 1057 | d.parent = node; 1058 | c.push(d); 1059 | v += d.value; 1060 | } 1061 | if (sort) c.sort(sort); 1062 | if (value) node.value = v; 1063 | } else if (value) { 1064 | node.value = +value.call(hierarchy, data, depth) || 0; 1065 | } 1066 | return node; 1067 | } 1068 | 1069 | // Recursively re-evaluates the node value. 1070 | function revalue(node, depth) { 1071 | var children = node.children, 1072 | v = 0; 1073 | if (children && (n = children.length)) { 1074 | var i = -1, 1075 | n, 1076 | j = depth + 1; 1077 | while (++i < n) v += revalue(children[i], j); 1078 | } else if (value) { 1079 | v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0; 1080 | } 1081 | if (value) node.value = v; 1082 | return v; 1083 | } 1084 | 1085 | function hierarchy(d) { 1086 | var nodes = []; 1087 | recurse(d, 0, nodes); 1088 | return nodes; 1089 | } 1090 | 1091 | hierarchy.sort = function(x) { 1092 | if (!arguments.length) return sort; 1093 | sort = x; 1094 | return hierarchy; 1095 | }; 1096 | 1097 | hierarchy.children = function(x) { 1098 | if (!arguments.length) return children; 1099 | children = x; 1100 | return hierarchy; 1101 | }; 1102 | 1103 | hierarchy.value = function(x) { 1104 | if (!arguments.length) return value; 1105 | value = x; 1106 | return hierarchy; 1107 | }; 1108 | 1109 | // Re-evaluates the `value` property for the specified hierarchy. 1110 | hierarchy.revalue = function(root) { 1111 | revalue(root, 0); 1112 | return root; 1113 | }; 1114 | 1115 | return hierarchy; 1116 | }; 1117 | 1118 | // A method assignment helper for hierarchy subclasses. 1119 | function d3_layout_hierarchyRebind(object, hierarchy) { 1120 | object.sort = d3.rebind(object, hierarchy.sort); 1121 | object.children = d3.rebind(object, hierarchy.children); 1122 | object.links = d3_layout_hierarchyLinks; 1123 | object.value = d3.rebind(object, hierarchy.value); 1124 | 1125 | // If the new API is used, enabling inlining. 1126 | object.nodes = function(d) { 1127 | d3_layout_hierarchyInline = true; 1128 | return (object.nodes = object)(d); 1129 | }; 1130 | 1131 | return object; 1132 | } 1133 | 1134 | function d3_layout_hierarchyChildren(d) { 1135 | return d.children; 1136 | } 1137 | 1138 | function d3_layout_hierarchyValue(d) { 1139 | return d.value; 1140 | } 1141 | 1142 | function d3_layout_hierarchySort(a, b) { 1143 | return b.value - a.value; 1144 | } 1145 | 1146 | // Returns an array source+target objects for the specified nodes. 1147 | function d3_layout_hierarchyLinks(nodes) { 1148 | return d3.merge(nodes.map(function(parent) { 1149 | return (parent.children || []).map(function(child) { 1150 | return {source: parent, target: child}; 1151 | }); 1152 | })); 1153 | } 1154 | 1155 | // For backwards-compatibility, don't enable inlining by default. 1156 | var d3_layout_hierarchyInline = false; 1157 | d3.layout.pack = function() { 1158 | var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), 1159 | size = [1, 1]; 1160 | 1161 | function pack(d, i) { 1162 | var nodes = hierarchy.call(this, d, i), 1163 | root = nodes[0]; 1164 | 1165 | // Recursively compute the layout. 1166 | root.x = 0; 1167 | root.y = 0; 1168 | d3_layout_packTree(root); 1169 | 1170 | // Scale the layout to fit the requested size. 1171 | var w = size[0], 1172 | h = size[1], 1173 | k = 1 / Math.max(2 * root.r / w, 2 * root.r / h); 1174 | d3_layout_packTransform(root, w / 2, h / 2, k); 1175 | 1176 | return nodes; 1177 | } 1178 | 1179 | pack.size = function(x) { 1180 | if (!arguments.length) return size; 1181 | size = x; 1182 | return pack; 1183 | }; 1184 | 1185 | return d3_layout_hierarchyRebind(pack, hierarchy); 1186 | }; 1187 | 1188 | function d3_layout_packSort(a, b) { 1189 | return a.value - b.value; 1190 | } 1191 | 1192 | function d3_layout_packInsert(a, b) { 1193 | var c = a._pack_next; 1194 | a._pack_next = b; 1195 | b._pack_prev = a; 1196 | b._pack_next = c; 1197 | c._pack_prev = b; 1198 | } 1199 | 1200 | function d3_layout_packSplice(a, b) { 1201 | a._pack_next = b; 1202 | b._pack_prev = a; 1203 | } 1204 | 1205 | function d3_layout_packIntersects(a, b) { 1206 | var dx = b.x - a.x, 1207 | dy = b.y - a.y, 1208 | dr = a.r + b.r; 1209 | return (dr * dr - dx * dx - dy * dy) > .001; // within epsilon 1210 | } 1211 | 1212 | function d3_layout_packCircle(nodes) { 1213 | var xMin = Infinity, 1214 | xMax = -Infinity, 1215 | yMin = Infinity, 1216 | yMax = -Infinity, 1217 | n = nodes.length, 1218 | a, b, c, j, k; 1219 | 1220 | function bound(node) { 1221 | xMin = Math.min(node.x - node.r, xMin); 1222 | xMax = Math.max(node.x + node.r, xMax); 1223 | yMin = Math.min(node.y - node.r, yMin); 1224 | yMax = Math.max(node.y + node.r, yMax); 1225 | } 1226 | 1227 | // Create node links. 1228 | nodes.forEach(d3_layout_packLink); 1229 | 1230 | // Create first node. 1231 | a = nodes[0]; 1232 | a.x = -a.r; 1233 | a.y = 0; 1234 | bound(a); 1235 | 1236 | // Create second node. 1237 | if (n > 1) { 1238 | b = nodes[1]; 1239 | b.x = b.r; 1240 | b.y = 0; 1241 | bound(b); 1242 | 1243 | // Create third node and build chain. 1244 | if (n > 2) { 1245 | c = nodes[2]; 1246 | d3_layout_packPlace(a, b, c); 1247 | bound(c); 1248 | d3_layout_packInsert(a, c); 1249 | a._pack_prev = c; 1250 | d3_layout_packInsert(c, b); 1251 | b = a._pack_next; 1252 | 1253 | // Now iterate through the rest. 1254 | for (var i = 3; i < n; i++) { 1255 | d3_layout_packPlace(a, b, c = nodes[i]); 1256 | 1257 | // Search for the closest intersection. 1258 | var isect = 0, s1 = 1, s2 = 1; 1259 | for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { 1260 | if (d3_layout_packIntersects(j, c)) { 1261 | isect = 1; 1262 | break; 1263 | } 1264 | } 1265 | if (isect == 1) { 1266 | for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { 1267 | if (d3_layout_packIntersects(k, c)) { 1268 | if (s2 < s1) { 1269 | isect = -1; 1270 | j = k; 1271 | } 1272 | break; 1273 | } 1274 | } 1275 | } 1276 | 1277 | // Update node chain. 1278 | if (isect == 0) { 1279 | d3_layout_packInsert(a, c); 1280 | b = c; 1281 | bound(c); 1282 | } else if (isect > 0) { 1283 | d3_layout_packSplice(a, j); 1284 | b = j; 1285 | i--; 1286 | } else { // isect < 0 1287 | d3_layout_packSplice(j, b); 1288 | a = j; 1289 | i--; 1290 | } 1291 | } 1292 | } 1293 | } 1294 | 1295 | // Re-center the circles and return the encompassing radius. 1296 | var cx = (xMin + xMax) / 2, 1297 | cy = (yMin + yMax) / 2, 1298 | cr = 0; 1299 | for (var i = 0; i < n; i++) { 1300 | var node = nodes[i]; 1301 | node.x -= cx; 1302 | node.y -= cy; 1303 | cr = Math.max(cr, node.r + Math.sqrt(node.x * node.x + node.y * node.y)); 1304 | } 1305 | 1306 | // Remove node links. 1307 | nodes.forEach(d3_layout_packUnlink); 1308 | 1309 | return cr; 1310 | } 1311 | 1312 | function d3_layout_packLink(node) { 1313 | node._pack_next = node._pack_prev = node; 1314 | } 1315 | 1316 | function d3_layout_packUnlink(node) { 1317 | delete node._pack_next; 1318 | delete node._pack_prev; 1319 | } 1320 | 1321 | function d3_layout_packTree(node) { 1322 | var children = node.children; 1323 | if (children && children.length) { 1324 | children.forEach(d3_layout_packTree); 1325 | node.r = d3_layout_packCircle(children); 1326 | } else { 1327 | node.r = Math.sqrt(node.value); 1328 | } 1329 | } 1330 | 1331 | function d3_layout_packTransform(node, x, y, k) { 1332 | var children = node.children; 1333 | node.x = (x += k * node.x); 1334 | node.y = (y += k * node.y); 1335 | node.r *= k; 1336 | if (children) { 1337 | var i = -1, n = children.length; 1338 | while (++i < n) d3_layout_packTransform(children[i], x, y, k); 1339 | } 1340 | } 1341 | 1342 | function d3_layout_packPlace(a, b, c) { 1343 | var db = a.r + c.r, 1344 | dx = b.x - a.x, 1345 | dy = b.y - a.y; 1346 | if (db && (dx || dy)) { 1347 | var da = b.r + c.r, 1348 | dc = Math.sqrt(dx * dx + dy * dy), 1349 | cos = Math.max(-1, Math.min(1, (db * db + dc * dc - da * da) / (2 * db * dc))), 1350 | theta = Math.acos(cos), 1351 | x = cos * (db /= dc), 1352 | y = Math.sin(theta) * db; 1353 | c.x = a.x + x * dx + y * dy; 1354 | c.y = a.y + x * dy - y * dx; 1355 | } else { 1356 | c.x = a.x + db; 1357 | c.y = a.y; 1358 | } 1359 | } 1360 | // Implements a hierarchical layout using the cluster (or dendogram) algorithm. 1361 | d3.layout.cluster = function() { 1362 | var hierarchy = d3.layout.hierarchy().sort(null).value(null), 1363 | separation = d3_layout_treeSeparation, 1364 | size = [1, 1]; // width, height 1365 | 1366 | function cluster(d, i) { 1367 | var nodes = hierarchy.call(this, d, i), 1368 | root = nodes[0], 1369 | previousNode, 1370 | x = 0, 1371 | kx, 1372 | ky; 1373 | 1374 | // First walk, computing the initial x & y values. 1375 | d3_layout_treeVisitAfter(root, function(node) { 1376 | var children = node.children; 1377 | if (children && children.length) { 1378 | node.x = d3_layout_clusterX(children); 1379 | node.y = d3_layout_clusterY(children); 1380 | } else { 1381 | node.x = previousNode ? x += separation(node, previousNode) : 0; 1382 | node.y = 0; 1383 | previousNode = node; 1384 | } 1385 | }); 1386 | 1387 | // Compute the left-most, right-most, and depth-most nodes for extents. 1388 | var left = d3_layout_clusterLeft(root), 1389 | right = d3_layout_clusterRight(root), 1390 | x0 = left.x - separation(left, right) / 2, 1391 | x1 = right.x + separation(right, left) / 2; 1392 | 1393 | // Second walk, normalizing x & y to the desired size. 1394 | d3_layout_treeVisitAfter(root, function(node) { 1395 | node.x = (node.x - x0) / (x1 - x0) * size[0]; 1396 | node.y = (1 - node.y / root.y) * size[1]; 1397 | }); 1398 | 1399 | return nodes; 1400 | } 1401 | 1402 | cluster.separation = function(x) { 1403 | if (!arguments.length) return separation; 1404 | separation = x; 1405 | return cluster; 1406 | }; 1407 | 1408 | cluster.size = function(x) { 1409 | if (!arguments.length) return size; 1410 | size = x; 1411 | return cluster; 1412 | }; 1413 | 1414 | return d3_layout_hierarchyRebind(cluster, hierarchy); 1415 | }; 1416 | 1417 | function d3_layout_clusterY(children) { 1418 | return 1 + d3.max(children, function(child) { 1419 | return child.y; 1420 | }); 1421 | } 1422 | 1423 | function d3_layout_clusterX(children) { 1424 | return children.reduce(function(x, child) { 1425 | return x + child.x; 1426 | }, 0) / children.length; 1427 | } 1428 | 1429 | function d3_layout_clusterLeft(node) { 1430 | var children = node.children; 1431 | return children && children.length ? d3_layout_clusterLeft(children[0]) : node; 1432 | } 1433 | 1434 | function d3_layout_clusterRight(node) { 1435 | var children = node.children, n; 1436 | return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; 1437 | } 1438 | // Node-link tree diagram using the Reingold-Tilford "tidy" algorithm 1439 | d3.layout.tree = function() { 1440 | var hierarchy = d3.layout.hierarchy().sort(null).value(null), 1441 | separation = d3_layout_treeSeparation, 1442 | size = [1, 1]; // width, height 1443 | 1444 | function tree(d, i) { 1445 | var nodes = hierarchy.call(this, d, i), 1446 | root = nodes[0]; 1447 | 1448 | function firstWalk(node, previousSibling) { 1449 | var children = node.children, 1450 | layout = node._tree; 1451 | if (children && (n = children.length)) { 1452 | var n, 1453 | firstChild = children[0], 1454 | previousChild, 1455 | ancestor = firstChild, 1456 | child, 1457 | i = -1; 1458 | while (++i < n) { 1459 | child = children[i]; 1460 | firstWalk(child, previousChild); 1461 | ancestor = apportion(child, previousChild, ancestor); 1462 | previousChild = child; 1463 | } 1464 | d3_layout_treeShift(node); 1465 | var midpoint = .5 * (firstChild._tree.prelim + child._tree.prelim); 1466 | if (previousSibling) { 1467 | layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); 1468 | layout.mod = layout.prelim - midpoint; 1469 | } else { 1470 | layout.prelim = midpoint; 1471 | } 1472 | } else { 1473 | if (previousSibling) { 1474 | layout.prelim = previousSibling._tree.prelim + separation(node, previousSibling); 1475 | } 1476 | } 1477 | } 1478 | 1479 | function secondWalk(node, x) { 1480 | node.x = node._tree.prelim + x; 1481 | var children = node.children; 1482 | if (children && (n = children.length)) { 1483 | var i = -1, 1484 | n; 1485 | x += node._tree.mod; 1486 | while (++i < n) { 1487 | secondWalk(children[i], x); 1488 | } 1489 | } 1490 | } 1491 | 1492 | function apportion(node, previousSibling, ancestor) { 1493 | if (previousSibling) { 1494 | var vip = node, 1495 | vop = node, 1496 | vim = previousSibling, 1497 | vom = node.parent.children[0], 1498 | sip = vip._tree.mod, 1499 | sop = vop._tree.mod, 1500 | sim = vim._tree.mod, 1501 | som = vom._tree.mod, 1502 | shift; 1503 | while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { 1504 | vom = d3_layout_treeLeft(vom); 1505 | vop = d3_layout_treeRight(vop); 1506 | vop._tree.ancestor = node; 1507 | shift = vim._tree.prelim + sim - vip._tree.prelim - sip + separation(vim, vip); 1508 | if (shift > 0) { 1509 | d3_layout_treeMove(d3_layout_treeAncestor(vim, node, ancestor), node, shift); 1510 | sip += shift; 1511 | sop += shift; 1512 | } 1513 | sim += vim._tree.mod; 1514 | sip += vip._tree.mod; 1515 | som += vom._tree.mod; 1516 | sop += vop._tree.mod; 1517 | } 1518 | if (vim && !d3_layout_treeRight(vop)) { 1519 | vop._tree.thread = vim; 1520 | vop._tree.mod += sim - sop; 1521 | } 1522 | if (vip && !d3_layout_treeLeft(vom)) { 1523 | vom._tree.thread = vip; 1524 | vom._tree.mod += sip - som; 1525 | ancestor = node; 1526 | } 1527 | } 1528 | return ancestor; 1529 | } 1530 | 1531 | // Initialize temporary layout variables. 1532 | d3_layout_treeVisitAfter(root, function(node, previousSibling) { 1533 | node._tree = { 1534 | ancestor: node, 1535 | prelim: 0, 1536 | mod: 0, 1537 | change: 0, 1538 | shift: 0, 1539 | number: previousSibling ? previousSibling._tree.number + 1 : 0 1540 | }; 1541 | }); 1542 | 1543 | // Compute the layout using Buchheim et al.'s algorithm. 1544 | firstWalk(root); 1545 | secondWalk(root, -root._tree.prelim); 1546 | 1547 | // Compute the left-most, right-most, and depth-most nodes for extents. 1548 | var left = d3_layout_treeSearch(root, d3_layout_treeLeftmost), 1549 | right = d3_layout_treeSearch(root, d3_layout_treeRightmost), 1550 | deep = d3_layout_treeSearch(root, d3_layout_treeDeepest), 1551 | x0 = left.x - separation(left, right) / 2, 1552 | x1 = right.x + separation(right, left) / 2, 1553 | y1 = deep.depth || 1; 1554 | 1555 | // Clear temporary layout variables; transform x and y. 1556 | d3_layout_treeVisitAfter(root, function(node) { 1557 | node.x = (node.x - x0) / (x1 - x0) * size[0]; 1558 | node.y = node.depth / y1 * size[1]; 1559 | delete node._tree; 1560 | }); 1561 | 1562 | return nodes; 1563 | } 1564 | 1565 | tree.separation = function(x) { 1566 | if (!arguments.length) return separation; 1567 | separation = x; 1568 | return tree; 1569 | }; 1570 | 1571 | tree.size = function(x) { 1572 | if (!arguments.length) return size; 1573 | size = x; 1574 | return tree; 1575 | }; 1576 | 1577 | return d3_layout_hierarchyRebind(tree, hierarchy); 1578 | }; 1579 | 1580 | function d3_layout_treeSeparation(a, b) { 1581 | return a.parent == b.parent ? 1 : 2; 1582 | } 1583 | 1584 | // function d3_layout_treeSeparationRadial(a, b) { 1585 | // return (a.parent == b.parent ? 1 : 2) / a.depth; 1586 | // } 1587 | 1588 | function d3_layout_treeLeft(node) { 1589 | var children = node.children; 1590 | return children && children.length ? children[0] : node._tree.thread; 1591 | } 1592 | 1593 | function d3_layout_treeRight(node) { 1594 | var children = node.children, 1595 | n; 1596 | return children && (n = children.length) ? children[n - 1] : node._tree.thread; 1597 | } 1598 | 1599 | function d3_layout_treeSearch(node, compare) { 1600 | var children = node.children; 1601 | if (children && (n = children.length)) { 1602 | var child, 1603 | n, 1604 | i = -1; 1605 | while (++i < n) { 1606 | if (compare(child = d3_layout_treeSearch(children[i], compare), node) > 0) { 1607 | node = child; 1608 | } 1609 | } 1610 | } 1611 | return node; 1612 | } 1613 | 1614 | function d3_layout_treeRightmost(a, b) { 1615 | return a.x - b.x; 1616 | } 1617 | 1618 | function d3_layout_treeLeftmost(a, b) { 1619 | return b.x - a.x; 1620 | } 1621 | 1622 | function d3_layout_treeDeepest(a, b) { 1623 | return a.depth - b.depth; 1624 | } 1625 | 1626 | function d3_layout_treeVisitAfter(node, callback) { 1627 | function visit(node, previousSibling) { 1628 | var children = node.children; 1629 | if (children && (n = children.length)) { 1630 | var child, 1631 | previousChild = null, 1632 | i = -1, 1633 | n; 1634 | while (++i < n) { 1635 | child = children[i]; 1636 | visit(child, previousChild); 1637 | previousChild = child; 1638 | } 1639 | } 1640 | callback(node, previousSibling); 1641 | } 1642 | visit(node, null); 1643 | } 1644 | 1645 | function d3_layout_treeShift(node) { 1646 | var shift = 0, 1647 | change = 0, 1648 | children = node.children, 1649 | i = children.length, 1650 | child; 1651 | while (--i >= 0) { 1652 | child = children[i]._tree; 1653 | child.prelim += shift; 1654 | child.mod += shift; 1655 | shift += child.shift + (change += child.change); 1656 | } 1657 | } 1658 | 1659 | function d3_layout_treeMove(ancestor, node, shift) { 1660 | ancestor = ancestor._tree; 1661 | node = node._tree; 1662 | var change = shift / (node.number - ancestor.number); 1663 | ancestor.change += change; 1664 | node.change -= change; 1665 | node.shift += shift; 1666 | node.prelim += shift; 1667 | node.mod += shift; 1668 | } 1669 | 1670 | function d3_layout_treeAncestor(vim, node, ancestor) { 1671 | return vim._tree.ancestor.parent == node.parent 1672 | ? vim._tree.ancestor 1673 | : ancestor; 1674 | } 1675 | // Squarified Treemaps by Mark Bruls, Kees Huizing, and Jarke J. van Wijk 1676 | // Modified to support a target aspect ratio by Jeff Heer 1677 | d3.layout.treemap = function() { 1678 | var hierarchy = d3.layout.hierarchy(), 1679 | round = Math.round, 1680 | size = [1, 1], // width, height 1681 | padding = null, 1682 | pad = d3_layout_treemapPadNull, 1683 | sticky = false, 1684 | stickies, 1685 | ratio = 0.5 * (1 + Math.sqrt(5)); // golden ratio 1686 | 1687 | // Compute the area for each child based on value & scale. 1688 | function scale(children, k) { 1689 | var i = -1, 1690 | n = children.length, 1691 | child, 1692 | area; 1693 | while (++i < n) { 1694 | area = (child = children[i]).value * (k < 0 ? 0 : k); 1695 | child.area = isNaN(area) || area <= 0 ? 0 : area; 1696 | } 1697 | } 1698 | 1699 | // Recursively arranges the specified node's children into squarified rows. 1700 | function squarify(node) { 1701 | var children = node.children; 1702 | if (children && children.length) { 1703 | var rect = pad(node), 1704 | row = [], 1705 | remaining = children.slice(), // copy-on-write 1706 | child, 1707 | best = Infinity, // the best row score so far 1708 | score, // the current row score 1709 | u = Math.min(rect.dx, rect.dy), // initial orientation 1710 | n; 1711 | scale(remaining, rect.dx * rect.dy / node.value); 1712 | row.area = 0; 1713 | while ((n = remaining.length) > 0) { 1714 | row.push(child = remaining[n - 1]); 1715 | row.area += child.area; 1716 | if ((score = worst(row, u)) <= best) { // continue with this orientation 1717 | remaining.pop(); 1718 | best = score; 1719 | } else { // abort, and try a different orientation 1720 | row.area -= row.pop().area; 1721 | position(row, u, rect, false); 1722 | u = Math.min(rect.dx, rect.dy); 1723 | row.length = row.area = 0; 1724 | best = Infinity; 1725 | } 1726 | } 1727 | if (row.length) { 1728 | position(row, u, rect, true); 1729 | row.length = row.area = 0; 1730 | } 1731 | children.forEach(squarify); 1732 | } 1733 | } 1734 | 1735 | // Recursively resizes the specified node's children into existing rows. 1736 | // Preserves the existing layout! 1737 | function stickify(node) { 1738 | var children = node.children; 1739 | if (children && children.length) { 1740 | var rect = pad(node), 1741 | remaining = children.slice(), // copy-on-write 1742 | child, 1743 | row = []; 1744 | scale(remaining, rect.dx * rect.dy / node.value); 1745 | row.area = 0; 1746 | while (child = remaining.pop()) { 1747 | row.push(child); 1748 | row.area += child.area; 1749 | if (child.z != null) { 1750 | position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); 1751 | row.length = row.area = 0; 1752 | } 1753 | } 1754 | children.forEach(stickify); 1755 | } 1756 | } 1757 | 1758 | // Computes the score for the specified row, as the worst aspect ratio. 1759 | function worst(row, u) { 1760 | var s = row.area, 1761 | r, 1762 | rmax = 0, 1763 | rmin = Infinity, 1764 | i = -1, 1765 | n = row.length; 1766 | while (++i < n) { 1767 | if (!(r = row[i].area)) continue; 1768 | if (r < rmin) rmin = r; 1769 | if (r > rmax) rmax = r; 1770 | } 1771 | s *= s; 1772 | u *= u; 1773 | return s 1774 | ? Math.max((u * rmax * ratio) / s, s / (u * rmin * ratio)) 1775 | : Infinity; 1776 | } 1777 | 1778 | // Positions the specified row of nodes. Modifies `rect`. 1779 | function position(row, u, rect, flush) { 1780 | var i = -1, 1781 | n = row.length, 1782 | x = rect.x, 1783 | y = rect.y, 1784 | v = u ? round(row.area / u) : 0, 1785 | o; 1786 | if (u == rect.dx) { // horizontal subdivision 1787 | if (flush || v > rect.dy) v = v ? rect.dy : 0; // over+underflow 1788 | while (++i < n) { 1789 | o = row[i]; 1790 | o.x = x; 1791 | o.y = y; 1792 | o.dy = v; 1793 | x += o.dx = v ? round(o.area / v) : 0; 1794 | } 1795 | o.z = true; 1796 | o.dx += rect.x + rect.dx - x; // rounding error 1797 | rect.y += v; 1798 | rect.dy -= v; 1799 | } else { // vertical subdivision 1800 | if (flush || v > rect.dx) v = v ? rect.dx : 0; // over+underflow 1801 | while (++i < n) { 1802 | o = row[i]; 1803 | o.x = x; 1804 | o.y = y; 1805 | o.dx = v; 1806 | y += o.dy = v ? round(o.area / v) : 0; 1807 | } 1808 | o.z = false; 1809 | o.dy += rect.y + rect.dy - y; // rounding error 1810 | rect.x += v; 1811 | rect.dx -= v; 1812 | } 1813 | } 1814 | 1815 | function treemap(d) { 1816 | var nodes = stickies || hierarchy(d), 1817 | root = nodes[0]; 1818 | root.x = 0; 1819 | root.y = 0; 1820 | root.dx = size[0]; 1821 | root.dy = size[1]; 1822 | if (stickies) hierarchy.revalue(root); 1823 | scale([root], root.dx * root.dy / root.value); 1824 | (stickies ? stickify : squarify)(root); 1825 | if (sticky) stickies = nodes; 1826 | return nodes; 1827 | } 1828 | 1829 | treemap.size = function(x) { 1830 | if (!arguments.length) return size; 1831 | size = x; 1832 | return treemap; 1833 | }; 1834 | 1835 | treemap.padding = function(x) { 1836 | if (!arguments.length) return padding; 1837 | 1838 | function padFunction(node) { 1839 | var p = x.call(treemap, node, node.depth); 1840 | return p == null 1841 | ? d3_layout_treemapPadNull(node) 1842 | : d3_layout_treemapPad(node, typeof p === "number" ? [p, p, p, p] : p); 1843 | } 1844 | 1845 | function padConstant(node) { 1846 | return d3_layout_treemapPad(node, x); 1847 | } 1848 | 1849 | var type; 1850 | pad = (padding = x) == null ? d3_layout_treemapPadNull 1851 | : (type = typeof x) === "function" ? padFunction 1852 | : type === "number" ? (x = [x, x, x, x], padConstant) 1853 | : padConstant; 1854 | return treemap; 1855 | }; 1856 | 1857 | treemap.round = function(x) { 1858 | if (!arguments.length) return round != Number; 1859 | round = x ? Math.round : Number; 1860 | return treemap; 1861 | }; 1862 | 1863 | treemap.sticky = function(x) { 1864 | if (!arguments.length) return sticky; 1865 | sticky = x; 1866 | stickies = null; 1867 | return treemap; 1868 | }; 1869 | 1870 | treemap.ratio = function(x) { 1871 | if (!arguments.length) return ratio; 1872 | ratio = x; 1873 | return treemap; 1874 | }; 1875 | 1876 | return d3_layout_hierarchyRebind(treemap, hierarchy); 1877 | }; 1878 | 1879 | function d3_layout_treemapPadNull(node) { 1880 | return {x: node.x, y: node.y, dx: node.dx, dy: node.dy}; 1881 | } 1882 | 1883 | function d3_layout_treemapPad(node, padding) { 1884 | var x = node.x + padding[3], 1885 | y = node.y + padding[0], 1886 | dx = node.dx - padding[1] - padding[3], 1887 | dy = node.dy - padding[0] - padding[2]; 1888 | if (dx < 0) { x += dx / 2; dx = 0; } 1889 | if (dy < 0) { y += dy / 2; dy = 0; } 1890 | return {x: x, y: y, dx: dx, dy: dy}; 1891 | } 1892 | })(); 1893 | -------------------------------------------------------------------------------- /app/assets/javascripts/links.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/tags.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/tweets.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/users.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 4 | 5 | $(document).ready -> 6 | w = 1600 7 | h = 1600 8 | fill = d3.scale.category20() 9 | 10 | vis = d3.select("#graph").append("svg:svg").attr("width", w).attr("height", h) 11 | 12 | d3.json("/users.json", (json) -> 13 | force = d3.layout.force() 14 | .charge(-220) 15 | .linkDistance(120) 16 | .nodes(json.nodes) 17 | .links(json.links) 18 | .size([w, h]) 19 | .start() 20 | 21 | link = vis.selectAll("line.link") 22 | .data(json.links) 23 | .enter().append("svg:line") 24 | .attr("class", "link") 25 | .style("stroke-width", (d) -> Math.sqrt(d.value)) 26 | .attr("x1", (d) -> d.source.x) 27 | .attr("y1", (d) -> d.source.y) 28 | .attr("x2", (d) -> d.target.x) 29 | .attr("y2", (d) -> d.target.y) 30 | 31 | node = vis.selectAll("g.node") 32 | .data(json.nodes) 33 | .enter().append("svg:g") 34 | .attr("transform", (d) -> "translate(" + d.x + "," + d.y + ")") 35 | .attr("class", "node") 36 | .call(force.drag) 37 | 38 | node.append("svg:circle") 39 | .attr("r", (d) -> if d.value > 25 then 50 else d.value*2 + 5) 40 | .style("fill", (d) -> '#fea') 41 | 42 | node.append("svg:title").text((d) -> d.name) 43 | node.append("svg:text") 44 | .attr("text-anchor", "middle") 45 | .attr("dy", ".3em") 46 | .text((d) -> d.name) 47 | 48 | vis.style("opacity", 1e-6) 49 | .transition() 50 | .duration(0) 51 | .style("opacity", 1) 52 | 53 | force.on("tick", -> 54 | link.attr("x1", (d) -> d.source.x).attr("y1", (d) -> d.source.y).attr("x2", (d) -> d.target.x).attr("y2", (d) -> d.target.y) 55 | node.attr("transform", (d) -> "translate(" + d.x + "," + d.y + ")") 56 | ) 57 | ) -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory 3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at 4 | * the top of the compiled file, but it's generally better to create a new file per style scope. 5 | *= require_self 6 | *= require_tree . 7 | */ 8 | 9 | circle.node { 10 | stroke: #fff; 11 | stroke-width: 1.5px; 12 | } 13 | 14 | line.link { 15 | stroke: #999; 16 | stroke-opacity: .6; 17 | } 18 | 19 | nav { 20 | float: left; 21 | left: 0; 22 | width: 100%; 23 | padding-bottom: 30px; 24 | } 25 | 26 | nav ul li { 27 | float: left; 28 | } 29 | 30 | nav ul li a { 31 | display: block; 32 | margin-right: 20px; 33 | width: 140px; 34 | font-size: 14px; 35 | line-height: 44px; 36 | text-align: center; 37 | text-decoration: none; 38 | color: #777; 39 | } 40 | 41 | nav ul li a:hover { 42 | color: #fff; 43 | } 44 | 45 | nav ul li.selected a { 46 | color: #fff; 47 | } 48 | 49 | nav ul li.subscribe a { 50 | margin-left: 22px; 51 | padding-left: 33px; 52 | text-align: left; 53 | } 54 | -------------------------------------------------------------------------------- /app/assets/stylesheets/links.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Links controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scaffolds.css.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | color: #333; 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; } 7 | 8 | p, ol, ul, td { 9 | font-family: verdana, arial, helvetica, sans-serif; 10 | font-size: 13px; 11 | line-height: 18px; } 12 | 13 | pre { 14 | background-color: #eee; 15 | padding: 10px; 16 | font-size: 11px; } 17 | 18 | a { 19 | color: #000; 20 | &:visited { 21 | color: #666; } 22 | &:hover { 23 | color: #fff; 24 | background-color: #000; } } 25 | 26 | div { 27 | &.field, &.actions { 28 | margin-bottom: 10px; } } 29 | 30 | #notice { 31 | color: green; } 32 | 33 | .field_with_errors { 34 | padding: 2px; 35 | background-color: red; 36 | display: table; } 37 | 38 | #error_explanation { 39 | width: 450px; 40 | border: 2px solid red; 41 | padding: 7px; 42 | padding-bottom: 0; 43 | margin-bottom: 20px; 44 | background-color: #f0f0f0; 45 | h2 { 46 | text-align: left; 47 | font-weight: bold; 48 | padding: 5px 5px 5px 15px; 49 | font-size: 12px; 50 | margin: -7px; 51 | margin-bottom: 0px; 52 | background-color: #c00; 53 | color: #fff; } 54 | ul li { 55 | font-size: 12px; 56 | list-style: square; } } 57 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tags.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Tags controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tweets.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Tweets controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/users.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/links_controller.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class LinksController < ApplicationController 3 | # GET /links 4 | # GET /links.json 5 | def index 6 | @links = Link.real 7 | 8 | respond_to do |format| 9 | format.html # index.html.erb 10 | format.json { render :json => @links } 11 | end 12 | end 13 | 14 | # GET /links/1 15 | # GET /links/1.json 16 | def show 17 | @link = Link.find(params[:id]) 18 | @tweets = @link._java_node.incoming(:links).incoming(:redirected_link).depth(2).filter{|path| path.lastRelationship.getType.to_s == 'links'}.paginate(:page => params[:page], :per_page => 10) 19 | 20 | respond_to do |format| 21 | format.html # show.html.erb 22 | format.json { render :json => @link } 23 | end 24 | end 25 | 26 | # GET /links/new 27 | # GET /links/new.json 28 | def new 29 | @link = Link.new 30 | 31 | respond_to do |format| 32 | format.html # new.html.erb 33 | format.json { render :json => @link } 34 | end 35 | end 36 | 37 | # GET /links/1/edit 38 | def edit 39 | @link = Link.find(params[:id]) 40 | end 41 | 42 | # POST /links 43 | # POST /links.json 44 | def create 45 | @link = Link.new(params[:link]) 46 | 47 | respond_to do |format| 48 | if @link.save 49 | format.html { redirect_to @link, :notice => 'Link was successfully created.' } 50 | format.json { render :json => @link, :status => :created, :location => @link } 51 | else 52 | format.html { render :action => "new" } 53 | format.json { render :json => @link.errors, :status => :unprocessable_entity } 54 | end 55 | end 56 | end 57 | 58 | # PUT /links/1 59 | # PUT /links/1.json 60 | def update 61 | @link = Link.find(params[:id]) 62 | 63 | respond_to do |format| 64 | if @link.update_attributes(params[:link]) 65 | format.html { redirect_to @link, :notice => 'Link was successfully updated.' } 66 | format.json { head :ok } 67 | else 68 | format.html { render :action => "edit" } 69 | format.json { render :json => @link.errors, :status => :unprocessable_entity } 70 | end 71 | end 72 | end 73 | 74 | # DELETE /links/1 75 | # DELETE /links/1.json 76 | def destroy 77 | @link = Link.find(params[:id]) 78 | @link.destroy 79 | 80 | respond_to do |format| 81 | format.html { redirect_to links_url } 82 | format.json { head :ok } 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /app/controllers/tags_controller.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class TagsController < ApplicationController 3 | # GET /tags 4 | # GET /tags.json 5 | def index 6 | @tags = Tag.all 7 | 8 | respond_to do |format| 9 | format.html # index.html.erb 10 | format.json { render :json => @tags } 11 | end 12 | end 13 | 14 | # GET /tags/1 15 | # GET /tags/1.json 16 | def show 17 | @tag = Tag.find(params[:id]) 18 | 19 | @tweets = @tag.tweets.paginate(:page => params[:page], :per_page => 20) 20 | 21 | respond_to do |format| 22 | format.html # show.html.erb 23 | format.json { render :json => @tag } 24 | end 25 | end 26 | 27 | # GET /tags/new 28 | # GET /tags/new.json 29 | def new 30 | @tag = Tag.new 31 | 32 | respond_to do |format| 33 | format.html # new.html.erb 34 | format.json { render :json => @tag } 35 | end 36 | end 37 | 38 | # GET /tags/1/edit 39 | def edit 40 | @tag = Tag.find(params[:id]) 41 | end 42 | 43 | # POST /tags 44 | # POST /tags.json 45 | def create 46 | @tag = Tag.new(params[:tag]) 47 | 48 | respond_to do |format| 49 | if @tag.save 50 | format.html { redirect_to @tag, :notice => 'Tag was successfully created.' } 51 | format.json { render :json => @tag, :status => :created, :location => @tag } 52 | else 53 | format.html { render :action => "new" } 54 | format.json { render :json => @tag.errors, :status => :unprocessable_entity } 55 | end 56 | end 57 | end 58 | 59 | # PUT /tags/1 60 | # PUT /tags/1.json 61 | def update 62 | @tag = Tag.find(params[:id]) 63 | 64 | respond_to do |format| 65 | if @tag.update_attributes(params[:tag]) 66 | format.html { redirect_to @tag, :notice => 'Tag was successfully updated.' } 67 | format.json { head :ok } 68 | else 69 | format.html { render :action => "edit" } 70 | format.json { render :json => @tag.errors, :status => :unprocessable_entity } 71 | end 72 | end 73 | end 74 | 75 | # DELETE /tags/1 76 | # DELETE /tags/1.json 77 | def destroy 78 | @tag = Tag.find(params[:id]) 79 | @tag.destroy 80 | 81 | respond_to do |format| 82 | format.html { redirect_to tags_url } 83 | format.json { head :ok } 84 | end 85 | end 86 | 87 | def search 88 | @tag = Tag.find(params[:id]) 89 | 90 | search = Twitter::Search.new 91 | result = search.hashtag(@tag.name) 92 | 93 | curr_page = 0 94 | while curr_page < 15 do 95 | result.each do |item| 96 | parsed_tweet_hash = Tweet.parse(item) 97 | next if Tweet.find_by_tweet_id(parsed_tweet_hash[:tweet_id]) 98 | tweet = Tweet.create!(parsed_tweet_hash) 99 | 100 | twid = item['from_user'].downcase 101 | user = User.find_or_create_by(:twid => twid) 102 | user.tweeted << tweet 103 | user.save 104 | 105 | parse_tweet(tweet, user) 106 | end 107 | result.fetch_next_page 108 | curr_page += 1 109 | end 110 | 111 | redirect_to @tag 112 | end 113 | 114 | 115 | def parse_tweet(tweet, user) 116 | tweet.text.gsub(/(@\w+|https?:\/\/[a-zA-Z0-9\-\.~\:\?#\[\]\!\@\$&,\*+=;,\/]+|#\w+)/).each do |t| 117 | case t 118 | when /^@.+/ 119 | t = t[1..-1].downcase 120 | other = User.find_or_create_by(:twid => t) 121 | user.knows << other unless t == user.twid || user.knows.include?(other) 122 | user.save 123 | tweet.mentions << other 124 | when /#.+/ 125 | t = t[1..-1].downcase 126 | tag = Tag.find_or_create_by(:name => t) 127 | tweet.tags << tag unless tweet.tags.include?(tag) 128 | user.used_tags << tag unless user.used_tags.include?(tag) 129 | user.save 130 | when /https?:.+/ 131 | link = Link.find_or_create_by(:url => t) 132 | tweet.links << (link.redirected_link || link) 133 | end 134 | end 135 | tweet.save! 136 | rescue 137 | end 138 | 139 | 140 | end 141 | -------------------------------------------------------------------------------- /app/controllers/tweets_controller.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class TweetsController < ApplicationController 3 | # GET /tweets 4 | # GET /tweets.json 5 | def index 6 | query = params[:query] 7 | if query.present? 8 | @tweets = Tweet.all("text:#{query}", :type => :fulltext).paginate(:page => params[:page], :per_page => 10) 9 | else 10 | @tweets = Tweet.all.paginate(:page => params[:page], :per_page => 10) 11 | end 12 | 13 | respond_to do |format| 14 | format.html # index.html.erb 15 | format.json { render :json => @tweets } 16 | end 17 | end 18 | 19 | # GET /tweets/1 20 | # GET /tweets/1.json 21 | def show 22 | @tweet = Tweet.find(params[:id]) 23 | 24 | respond_to do |format| 25 | format.html # show.html.erb 26 | format.json { render :json => @tweet } 27 | end 28 | end 29 | 30 | # GET /tweets/new 31 | # GET /tweets/new.json 32 | def new 33 | @tweet = Tweet.new 34 | 35 | respond_to do |format| 36 | format.html # new.html.erb 37 | format.json { render :json => @tweet } 38 | end 39 | end 40 | 41 | # GET /tweets/1/edit 42 | def edit 43 | @tweet = Tweet.find(params[:id]) 44 | end 45 | 46 | # POST /tweets 47 | # POST /tweets.json 48 | def create 49 | @tweet = Tweet.new(params[:tweet]) 50 | 51 | respond_to do |format| 52 | if @tweet.save 53 | format.html { redirect_to @tweet, :notice => 'Tweet was successfully created.' } 54 | format.json { render :json => @tweet, :status => :created, :location => @tweet } 55 | else 56 | format.html { render :action => "new" } 57 | format.json { render :json => @tweet.errors, :status => :unprocessable_entity } 58 | end 59 | end 60 | end 61 | 62 | # PUT /tweets/1 63 | # PUT /tweets/1.json 64 | def update 65 | @tweet = Tweet.find(params[:id]) 66 | 67 | respond_to do |format| 68 | if @tweet.update_attributes(params[:tweet]) 69 | format.html { redirect_to @tweet, :notice => 'Tweet was successfully updated.' } 70 | format.json { head :ok } 71 | else 72 | format.html { render :action => "edit" } 73 | format.json { render :json => @tweet.errors, :status => :unprocessable_entity } 74 | end 75 | end 76 | end 77 | 78 | # DELETE /tweets/1 79 | # DELETE /tweets/1.json 80 | def destroy 81 | @tweet = Tweet.find(params[:id]) 82 | @tweet.destroy 83 | 84 | respond_to do |format| 85 | format.html { redirect_to tweets_url } 86 | format.json { head :ok } 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class UsersController < ApplicationController 3 | # GET /users 4 | # GET /users.json 5 | def index 6 | respond_to do |format| 7 | format.html do 8 | query = params[:query] 9 | @user = User.find_by_twid(query) if query.present? 10 | redirect_to @user if @user 11 | end 12 | format.json do 13 | @users = User.all 14 | nodes = @users.map{|u| {:name => u.twid, :value => u.tweeted.count}} 15 | links = [] 16 | @users.each do |user| 17 | links += user.knows.map {|other| { :source => nodes.find_index{|n| n[:name] == user.twid}, :target => nodes.find_index{|n| n[:name] == other.twid}}} 18 | end 19 | render :json => {:nodes => nodes, :links => links} 20 | end 21 | end 22 | end 23 | 24 | # GET /users/1 25 | # GET /users/1.json 26 | def show 27 | @user = User.find(params[:id]) 28 | @knows = @user.knows.paginate(:page => params[:page], :per_page => 10) 29 | @recommend = recommend(@user) 30 | @mentioned_from = @user.mentioned_from 31 | 32 | respond_to do |format| 33 | format.html # show.html.erb 34 | format.json { render :json => @user } 35 | end 36 | end 37 | 38 | # GET /users/new 39 | # GET /users/new.json 40 | def new 41 | @user = User.new 42 | 43 | respond_to do |format| 44 | format.html # new.html.erb 45 | format.json { render :json => @user } 46 | end 47 | end 48 | 49 | # GET /users/1/edit 50 | def edit 51 | @user = User.find(params[:id]) 52 | end 53 | 54 | # POST /users 55 | # POST /users.json 56 | def create 57 | @user = User.new(params[:user]) 58 | 59 | respond_to do |format| 60 | if @user.save 61 | format.html { redirect_to @user, :notice => 'User was successfully created.' } 62 | format.json { render :json => @user, :status => :created, :location => @user } 63 | else 64 | format.html { render :action => "new" } 65 | format.json { render :json => @user.errors, :status => :unprocessable_entity } 66 | end 67 | end 68 | end 69 | 70 | # PUT /users/1 71 | # PUT /users/1.json 72 | def update 73 | @user = User.find(params[:id]) 74 | 75 | respond_to do |format| 76 | if @user.update_attributes(params[:user]) 77 | format.html { redirect_to @user, :notice => 'User was successfully updated.' } 78 | format.json { head :ok } 79 | else 80 | format.html { render :action => "edit" } 81 | format.json { render :json => @user.errors, :status => :unprocessable_entity } 82 | end 83 | end 84 | end 85 | 86 | # DELETE /users/1 87 | # DELETE /users/1.json 88 | def destroy 89 | @user = User.find(params[:id]) 90 | @user.destroy 91 | 92 | respond_to do |format| 93 | format.html { redirect_to users_url } 94 | format.json { head :ok } 95 | end 96 | end 97 | 98 | private 99 | 100 | def recommend(user) 101 | my_tags = user.used_tags.to_a 102 | my_friends = user.knows.to_a 103 | # find my users tags and all people using those tags, but exclude my friends 104 | # we are here using the raw java API - that's why using _java_node, raw and wrapper 105 | friends_friends = user._java_node.outgoing(:used_tags).incoming(:used_tags).raw.depth(2).filter{|path| path.length == 2 && !my_friends.include?(path.end_node)} 106 | # for all those people, find the person who has the max number of same tags as I have 107 | found = friends_friends.max_by{|friend| (friend.outgoing(:used_tags).raw.map{|tag| tag[:name]} & my_tags).size } 108 | found && found.wrapper # load the ruby wrapper around the neo4j java node 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/links_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module LinksHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/tags_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module TagsHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/tweets_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module TweetsHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | module UsersHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/app/mailers/.gitkeep -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/app/models/.gitkeep -------------------------------------------------------------------------------- /app/models/link.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Link < Neo4j::Rails::Model 3 | property :url, :type => String, :index =>:exact 4 | has_n(:tweets).from(:links) 5 | has_one(:redirected_link) 6 | has_n(:short_urls).from(:redirected_link) 7 | 8 | SHORT_URLS = %w[t.co bit.ly ow.ly goo.gl tiny.cc tinyurl.com doiop.com readthisurl.com memurl.com tr.im cli.gs short.ie kl.am idek.net short.ie is.gd hex.io asterl.in j.mp] .to_set 9 | 10 | rule(:real) { redirected_link.nil?} 11 | 12 | before_save :create_redirect_link 13 | 14 | def to_s 15 | url 16 | end 17 | 18 | private 19 | 20 | def self.short_url?(url) 21 | domain = url.split('/')[2] 22 | domain && SHORT_URLS.include?(domain) 23 | end 24 | 25 | def create_redirect_link 26 | return if !self.class.short_url?(url) 27 | uri = URI.parse(url) 28 | http = Net::HTTP.new(uri.host, uri.port) 29 | http.read_timeout = 200 30 | req = Net::HTTP::Head.new(uri.request_uri) 31 | res = http.request(req) 32 | redirect = res['location'] 33 | if redirect && url != redirect 34 | self.redirected_link = Link.find_or_create_by(:url => redirect.strip) 35 | end 36 | rescue Timeout::Error 37 | puts "Can't acccess #{url}" 38 | rescue Error 39 | puts "Can't call #{url}" 40 | rescue Net::HTTPBadResponse 41 | puts "Bad response for #{url}" 42 | end 43 | 44 | 45 | end 46 | -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Tag < Neo4j::Rails::Model 3 | property :name, :type => String, :index => :exact 4 | 5 | has_n(:tweets).from(:tags) 6 | 7 | has_n(:used_by_users).from(:used_tags) 8 | 9 | def to_s 10 | name 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/tweet.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class Tweet < Neo4j::Rails::Model 3 | property :text, :type => String, :index => :fulltext 4 | property :link, :type => String 5 | property :date, :type => DateTime, :index => :exact 6 | property :tweet_id, :type => String, :index => :exact 7 | 8 | has_n :tags 9 | has_n :mentions 10 | has_n :links 11 | has_one(:tweeted_by).from(:tweeted) 12 | 13 | 14 | def to_s 15 | text.gsub(/(@\w+|https?\S+|#\w+)/,"").strip 16 | end 17 | 18 | def self.parse(item) 19 | {:tweet_id => item['id_str'], 20 | :text => item['text'], 21 | :date => Time.parse(item['created_at']), 22 | :link => "http://twitter.com/#{item['from_user']}/statuses/#{item['id_str']}" 23 | } 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | class User < Neo4j::Rails::Model 3 | property :twid, :type => String, :index => :exact 4 | property :link, :type => String 5 | 6 | has_n :tweeted 7 | has_n :follows 8 | has_n :knows 9 | has_n :used_tags 10 | has_n(:mentioned_from).from(:mentions) 11 | 12 | def to_s 13 | twid 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kvitter 5 | <%= javascript_include_tag "application" %> 6 | <%= stylesheet_link_tag "application"%> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | 19 | 20 | <%= yield %> 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/views/links/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@link) do |f| %> 2 | <% if @link.errors.any? %> 3 |
4 |

<%= pluralize(@link.errors.count, "error") %> prohibited this link from being saved:

5 | 6 |
    7 | <% @link.errors.full_messages.each do |msg| %> 8 |
  • <%= msg %>
  • 9 | <% end %> 10 |
11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :url %>
16 | <%= f.text_field :url %> 17 |
18 | 19 |
20 | <%= f.submit %> 21 |
22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/links/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing link

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @link %> | 6 | <%= link_to 'Back', links_path %> 7 | -------------------------------------------------------------------------------- /app/views/links/index.html.erb: -------------------------------------------------------------------------------- 1 |

Listing links

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @links.each do |link| %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 |
Url
<%= link.url %><%= link_to 'Show', link %><%= link_to 'Edit', edit_link_path(link) %><%= link_to 'Destroy', link, :confirm => 'Are you sure?', :method => :delete %>
20 | 21 |
22 | 23 | <%= link_to 'New Link', new_link_path %> 24 | -------------------------------------------------------------------------------- /app/views/links/new.html.erb: -------------------------------------------------------------------------------- 1 |

New link

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', links_path %> 6 | -------------------------------------------------------------------------------- /app/views/links/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 |

4 | Url: 5 | <%= link_to @link.url, @link.url %> 6 |

7 | 8 |

9 | Short Urls: 10 | <% @link.short_urls.each do |link| %> 11 | <%= link_to link, link %>
12 | <% end %> 13 |

14 | 15 | <% if @link.redirected_link %> 16 |

17 | Redirects to 18 | <%= link_to @link.redirected_link, @link.redirected_link %> 19 |

20 | <% end %> 21 | 22 |

23 | Tweets:
24 | <% @tweets.each do |tweet| %> 25 | <%= link_to tweet, tweet %>
26 | <% end %> 27 | <%= will_paginate(@tweets) %> 28 |

29 | 30 | <%= link_to 'Edit', edit_link_path(@link) %> | 31 | <%= link_to 'Back', links_path %> 32 | -------------------------------------------------------------------------------- /app/views/tags/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@tag) do |f| %> 2 | <% if @tag.errors.any? %> 3 |
4 |

<%= pluralize(@tag.errors.count, "error") %> prohibited this tag from being saved:

5 | 6 |
    7 | <% @tag.errors.full_messages.each do |msg| %> 8 |
  • <%= msg %>
  • 9 | <% end %> 10 |
11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :name %>
16 | <%= f.text_field :name %> 17 |
18 |
19 | <%= f.submit %> 20 |
21 | <% end %> 22 | -------------------------------------------------------------------------------- /app/views/tags/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing tag

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @tag %> | 6 | <%= link_to 'Back', tags_path %> 7 | -------------------------------------------------------------------------------- /app/views/tags/index.html.erb: -------------------------------------------------------------------------------- 1 |

Listing tags

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <% @tags.each do |tag| %> 12 | 13 | 14 | 15 | 16 | 17 | 18 | <% end %> 19 |
Name
<%= tag.name %><%= link_to 'Show', tag %><%= link_to 'Edit', edit_tag_path(tag) %><%= link_to 'Destroy', tag, :confirm => 'Are you sure?', :method => :delete %>
20 | 21 |
22 | 23 | <%= link_to 'New Tag', new_tag_path %> 24 | -------------------------------------------------------------------------------- /app/views/tags/new.html.erb: -------------------------------------------------------------------------------- 1 |

New tag

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', tags_path %> 6 | -------------------------------------------------------------------------------- /app/views/tags/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 |

4 | Name: 5 | <%= @tag.name %> 6 |

7 | 8 |

9 | Tweets:
10 | <% @tweets.each do |tweet| %> 11 | <%= link_to tweet, tweet %>
12 | <% end %> 13 | <%= will_paginate(@tweets) %> 14 |

15 | 16 | <%= button_to "Search", [:search, @tag], :method => :get %> 17 | 18 | 19 | <%= link_to 'Edit', edit_tag_path(@tag) %> | 20 | <%= link_to 'Back', tags_path %> 21 | -------------------------------------------------------------------------------- /app/views/tweets/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@tweet) do |f| %> 2 | <% if @tweet.errors.any? %> 3 |
4 |

<%= pluralize(@tweet.errors.count, "error") %> prohibited this tweet from being saved:

5 | 6 |
    7 | <% @tweet.errors.full_messages.each do |msg| %> 8 |
  • <%= msg %>
  • 9 | <% end %> 10 |
11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :text %>
16 | <%= f.text_field :text %> 17 |
18 |
19 | <%= f.label :link %>
20 | <%= f.text_field :link %> 21 |
22 |
23 | <%= f.label :date %>
24 | <%= f.datetime_select :date %> 25 |
26 |
27 | <%= f.label :tweet_id %>
28 | <%= f.text_field :tweet_id %> 29 |
30 |
31 | <%= f.submit %> 32 |
33 | <% end %> 34 | -------------------------------------------------------------------------------- /app/views/tweets/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing tweet

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @tweet %> | 6 | <%= link_to 'Back', tweets_path %> 7 | -------------------------------------------------------------------------------- /app/views/tweets/index.html.erb: -------------------------------------------------------------------------------- 1 |

Listing tweets

2 | 3 | 4 | <%= form_for(:tweets, :method => :get) do |f| %> 5 |
6 | <%= text_field_tag :query %> 7 | <%= f.submit "Search" %> 8 |
9 | <% end %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% @tweets.each do |tweet| %> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% end %> 33 |
TextLinkDateTweet
<%= tweet.text %><%= tweet.link %><%= tweet.date %><%= tweet.tweet_id %><%= link_to 'Show', tweet %><%= link_to 'Edit', edit_tweet_path(tweet) %><%= link_to 'Destroy', tweet, :confirm => 'Are you sure?', :method => :delete %>
34 | 35 |
36 | 37 | <%= will_paginate(@tweets) %> 38 | 39 | <%= link_to 'New Tweet', new_tweet_path %> 40 | -------------------------------------------------------------------------------- /app/views/tweets/new.html.erb: -------------------------------------------------------------------------------- 1 |

New tweet

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', tweets_path %> 6 | -------------------------------------------------------------------------------- /app/views/tweets/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 |

4 | Text: 5 | <%= @tweet.text %> 6 |

7 | 8 |

9 | Link: 10 | <%= link_to @tweet.link, @tweet.link %> 11 |

12 | 13 |

14 | Date: 15 | <%= @tweet.date %> 16 |

17 | 18 |

19 | Tweet: 20 | <%= @tweet.tweet_id %> 21 |

22 | 23 | 24 |

25 | Tweeted by 26 | <%= link_to @tweet.tweeted_by, @tweet.tweeted_by %> 27 |

28 | 29 |

30 | Tags:
31 | <% @tweet.tags.each do |tag| %> 32 | <%= link_to tag, tag %>
33 | <% end %> 34 |

35 | 36 | 37 |

38 | Links:
39 | <% @tweet.links.each do |link| %> 40 | <%= link_to link, link %>
41 | <% end %> 42 |

43 | 44 | 45 | 46 | <%= link_to 'Edit', edit_tweet_path(@tweet) %> | 47 | <%= link_to 'Back', tweets_path %> 48 | -------------------------------------------------------------------------------- /app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@user) do |f| %> 2 | <% if @user.errors.any? %> 3 |
4 |

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

5 | 6 |
    7 | <% @user.errors.full_messages.each do |msg| %> 8 |
  • <%= msg %>
  • 9 | <% end %> 10 |
11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :twid %>
16 | <%= f.text_field :twid %> 17 |
18 |
19 | <%= f.label :link %>
20 | <%= f.text_field :link %> 21 |
22 |
23 | <%= f.submit %> 24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Show', @user %> | 6 | <%= link_to 'Back', users_path %> 7 | -------------------------------------------------------------------------------- /app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(:tweets, :method => :get) do |f| %> 2 |
3 | <%= text_field_tag :query %> 4 | <%= f.submit "Search user" %> 5 |
6 | <% end %> 7 | 8 |
9 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |

New user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', users_path %> 6 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 | 3 |

4 | Twid: 5 | <%= @user.twid %> 6 |

7 | 8 |

9 | Link: 10 | <%= @user.link %> 11 |

12 | 13 | <% if @recommend %> 14 |

15 | Recommend 16 | <%= link_to @recommend.twid, @recommend %> 17 |

18 | <% end %> 19 | 20 |

21 | Used tags 22 | <% @user.used_tags.each do |tag| %> 23 | <%= link_to tag.name, tag %>
24 | <% end %> 25 |

26 | 27 | 28 |

29 | Knows:
30 | <% @knows.each do |user| %> 31 | <%= link_to user, user %>
32 | <% end %> 33 | <%= will_paginate(@knows) %> 34 |

35 | 36 |

37 | Mentioned from:
38 | <% @mentioned_from.each do |tweet| %> 39 | <%= link_to tweet, tweet %>
40 | <% end %> 41 |

42 | 43 |

44 | Tweets
45 | <% @user.tweeted.each do |tweet| %> 46 | <%= link_to tweet, tweet %>
47 | <% end %> 48 |

49 | 50 | 51 | 52 | <%= link_to 'Edit', edit_user_path(@user) %> | 53 | <%= link_to 'Back', users_path %> 54 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Kvitter::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # -*- coding: undecided -*- 3 | require File.expand_path('../boot', __FILE__) 4 | 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "active_resource/railtie" 8 | require "rails/test_unit/railtie" 9 | require 'sprockets/railtie' 10 | require 'neo4j' 11 | 12 | 13 | if defined?(Bundler) 14 | # If you precompile assets before deploying to production, use this line 15 | Bundler.require(*Rails.groups(:assets => %w(development test))) 16 | # If you want your assets lazily compiled in production, use this line 17 | # Bundler.require(:default, :assets, Rails.env) 18 | end 19 | 20 | module Kvitter 21 | class Application < Rails::Application 22 | # Settings in config/environments/* take precedence over those specified here. 23 | # Application configuration should go into files in config/initializers 24 | # -- all .rb files in that directory are automatically loaded. 25 | 26 | # Custom directories with classes and modules you want to be autoloadable. 27 | # config.autoload_paths += %W(#{config.root}/extras) 28 | 29 | # Only load the plugins named here, in the order given (default is alphabetical). 30 | # :all can be used as a placeholder for all plugins not explicitly named. 31 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 32 | 33 | # Activate observers that should always be running. 34 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 35 | 36 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 37 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 38 | # config.time_zone = 'Central Time (US & Canada)' 39 | 40 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 41 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 42 | # config.i18n.default_locale = :de 43 | 44 | # Configure the default encoding used in templates for Ruby 1.9. 45 | config.encoding = "utf-8" 46 | 47 | # Configure sensitive parameters which will be filtered from the log file. 48 | config.filter_parameters += [:password] 49 | 50 | # Enable Neo4j generators, e.g: rails generate model Admin --parent User 51 | config.generators do |g| 52 | g.orm :neo4j 53 | g.test_framework :rspec, :fixture => false 54 | end 55 | 56 | # Configure where the neo4j database should exist 57 | config.neo4j.storage_path = "#{config.root}/db/neo4j-#{Rails.env}" 58 | 59 | config.neo4j.identity_map = false 60 | 61 | # Enable the asset pipeline 62 | config.assets.enabled = true 63 | 64 | # Version of your assets, change this if you want to expire all your assets 65 | config.assets.version = '1.0' 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'rubygems' 3 | 4 | # Set up gems listed in the Gemfile. 5 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 6 | 7 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 8 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem 'activerecord-jdbcsqlite3-adapter' 3 | # 4 | # Configure Using Gemfile 5 | # gem 'activerecord-jdbcsqlite3-adapter' 6 | # 7 | development: 8 | adapter: sqlite3 9 | database: db/development.sqlite3 10 | 11 | # Warning: The database defined as "test" will be erased and 12 | # re-generated from your development database when you run "rake". 13 | # Do not set this db to the same as development or production. 14 | test: 15 | adapter: sqlite3 16 | database: db/test.sqlite3 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Load the rails application 3 | require File.expand_path('../application', __FILE__) 4 | 5 | # Initialize the rails application 6 | Kvitter::Application.initialize! 7 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | Kvitter::Application.configure do 3 | # Settings specified here will take precedence over those in config/application.rb 4 | 5 | # In the development environment your application's code is reloaded on 6 | # every request. This slows down response time but is perfect for development 7 | # since you don't have to restart the web server when you make code changes. 8 | config.cache_classes = false 9 | 10 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | 26 | # Do not compress assets 27 | config.assets.compress = false 28 | 29 | # Expands the lines which load the assets 30 | config.assets.debug = true 31 | end 32 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | Kvitter::Application.configure do 3 | # Settings specified here will take precedence over those in config/application.rb 4 | 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Disable Rails's static asset server (Apache or nginx will already do this) 13 | config.serve_static_assets = true 14 | 15 | # Compress JavaScripts and CSS 16 | config.assets.compress = true 17 | 18 | # Don't fallback to assets pipeline if a precompiled asset is missed 19 | config.assets.compile = false 20 | 21 | # Generate digests for assets URLs 22 | config.assets.digest = true 23 | 24 | # Defaults to Rails.root.join("public/assets") 25 | # config.assets.manifest = YOUR_PATH 26 | 27 | # Specifies the header that your server uses for sending files 28 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 29 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 30 | 31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 32 | # config.force_ssl = true 33 | 34 | # See everything in the log (default is :info) 35 | # config.log_level = :debug 36 | 37 | # Use a different logger for distributed setups 38 | # config.logger = SyslogLogger.new 39 | 40 | # Use a different cache store in production 41 | # config.cache_store = :mem_cache_store 42 | 43 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 44 | # config.action_controller.asset_host = "http://assets.example.com" 45 | 46 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 47 | # config.assets.precompile += %w( search.js ) 48 | 49 | # Disable delivery errors, bad email addresses will be ignored 50 | # config.action_mailer.raise_delivery_errors = false 51 | 52 | # Enable threaded mode 53 | config.threadsafe! 54 | 55 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 56 | # the I18n.default_locale when a translation can not be found) 57 | config.i18n.fallbacks = true 58 | 59 | # Send deprecation notices to registered listeners 60 | config.active_support.deprecation = :notify 61 | end 62 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | Kvitter::Application.configure do 3 | # Settings specified here will take precedence over those in config/application.rb 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | config.cache_classes = true 10 | 11 | # Configure static asset server for tests with Cache-Control for performance 12 | config.serve_static_assets = true 13 | config.static_cache_control = "public, max-age=3600" 14 | 15 | # Log error messages when you accidentally call methods on nil 16 | config.whiny_nils = true 17 | 18 | # Show full error reports and disable caching 19 | config.consider_all_requests_local = true 20 | config.action_controller.perform_caching = false 21 | 22 | # Raise exceptions instead of rendering exception templates 23 | config.action_dispatch.show_exceptions = false 24 | 25 | # Disable request forgery protection in test environment 26 | config.action_controller.allow_forgery_protection = false 27 | 28 | # Tell Action Mailer not to deliver emails to the real world. 29 | # The :test delivery method accumulates sent emails in the 30 | # ActionMailer::Base.deliveries array. 31 | config.action_mailer.delivery_method = :test 32 | 33 | # Use SQL instead of Active Record's schema dumper when creating the test database. 34 | # This is necessary if your schema can't be completely dumped by the schema dumper, 35 | # like if you have constraints or database-specific column types 36 | # config.active_record.schema_format = :sql 37 | 38 | # Print deprecation notices to the stderr 39 | config.active_support.deprecation = :stderr 40 | end 41 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 6 | 7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 8 | # Rails.backtrace_cleaner.remove_silencers! 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format 5 | # (all these examples are active by default): 6 | # ActiveSupport::Inflector.inflections do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | # Mime::Type.register_alias "text/html", :iphone 7 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Your secret key for verifying the integrity of signed cookies. 5 | # If you change this key, all old signed cookies will become invalid! 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | Kvitter::Application.config.secret_token = '025c6ea650ad4a77606eb82ca9bcd5decdfb5277ed6d7bbb78406232376a8b183fd6a05d9501754df9aceee58bdc6420f52023b59576e850ed7787b3d3d86b70' 9 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | 4 | Kvitter::Application.config.session_store :cookie_store, :key => '_kvitter_session' 5 | 6 | # Use the database for sessions instead of the cookie-based default, 7 | # which shouldn't be used to store highly confidential information 8 | # (create the session table with "rails generate session_migration") 9 | # Kvitter::Application.config.session_store :active_record_store 10 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # Be sure to restart your server when you modify this file. 3 | # 4 | # This file contains settings for ActionController::ParamsWrapper which 5 | # is enabled by default. 6 | 7 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 8 | ActiveSupport.on_load(:action_controller) do 9 | wrap_parameters :format => [:json] 10 | end 11 | 12 | # Disable root element in JSON by default. 13 | ActiveSupport.on_load(:active_record) do 14 | self.include_root_in_json = false 15 | end 16 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | Kvitter::Application.routes.draw do 3 | 4 | resources :tweets 5 | 6 | resources :tags do 7 | get :search, :on => :member 8 | end 9 | 10 | resources :links 11 | 12 | resources :users 13 | 14 | root :to => 'tags#index' 15 | 16 | # The priority is based upon order of creation: 17 | # first created -> highest priority. 18 | 19 | # Sample of regular route: 20 | # match 'products/:id' => 'catalog#view' 21 | # Keep in mind you can assign values other than :controller and :action 22 | 23 | # Sample of named route: 24 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 25 | # This route can be invoked with purchase_url(:id => product.id) 26 | 27 | # Sample resource route (maps HTTP verbs to controller actions automatically): 28 | # resources :products 29 | 30 | # Sample resource route with options: 31 | # resources :products do 32 | # member do 33 | # get 'short' 34 | # post 'toggle' 35 | # end 36 | # 37 | # collection do 38 | # get 'sold' 39 | # end 40 | # end 41 | 42 | # Sample resource route with sub-resources: 43 | # resources :products do 44 | # resources :comments, :sales 45 | # resource :seller 46 | # end 47 | 48 | # Sample resource route with more complex sub-resources 49 | # resources :products do 50 | # resources :comments 51 | # resources :sales do 52 | # get 'recent', :on => :collection 53 | # end 54 | # end 55 | 56 | # Sample resource route within a namespace: 57 | # namespace :admin do 58 | # # Directs /admin/products/* to Admin::ProductsController 59 | # # (app/controllers/admin/products_controller.rb) 60 | # resources :products 61 | # end 62 | 63 | # You can have the root of your site routed with "root" 64 | # just remember to delete public/index.html. 65 | # root :to => 'welcome#index' 66 | 67 | # See how all your routes lay out with "rake routes" 68 | 69 | # This is a legacy wild controller route that's not recommended for RESTful applications. 70 | # Note: This route will make all actions in every controller accessible via GET requests. 71 | # match ':controller(/:action(/:id(.:format)))' 72 | end 73 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | # This file should contain all the record creation needed to seed the database with its default values. 3 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 4 | # 5 | # Examples: 6 | # 7 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 8 | # Mayor.create(:name => 'Emanuel', :city => cities.first) 9 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/lib/assets/.gitkeep -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/log/.gitkeep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/controllers/links_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # This spec was generated by rspec-rails when you ran the scaffold generator. 5 | # It demonstrates how one might use RSpec to specify the controller code that 6 | # was generated by Rails when you ran the scaffold generator. 7 | # 8 | # It assumes that the implementation code is generated by the rails scaffold 9 | # generator. If you are using any extension libraries to generate different 10 | # controller code, this generated spec may or may not pass. 11 | # 12 | # It only uses APIs available in rails and/or rspec-rails. There are a number 13 | # of tools you can use to make these specs even more expressive, but we're 14 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 15 | # 16 | # Compared to earlier versions of this generator, there is very limited use of 17 | # stubs and message expectations in this spec. Stubs are only used when there 18 | # is no simpler way to get a handle on the object needed for the example. 19 | # Message expectations are only used when there is no simpler way to specify 20 | # that an instance is receiving a specific message. 21 | 22 | describe LinksController do 23 | 24 | # This should return the minimal set of attributes required to create a valid 25 | # Link. As you add validations to Link, be sure to 26 | # update the return value of this method accordingly. 27 | def valid_attributes 28 | {} 29 | end 30 | 31 | describe "GET index" do 32 | it "assigns all links as @links" do 33 | link = Link.create! valid_attributes 34 | get :index 35 | assigns(:links).should eq([link]) 36 | end 37 | end 38 | 39 | describe "GET show" do 40 | it "assigns the requested link as @link" do 41 | link = Link.create! valid_attributes 42 | get :show, :id => link.id 43 | assigns(:link).should eq(link) 44 | end 45 | end 46 | 47 | describe "GET new" do 48 | it "assigns a new link as @link" do 49 | get :new 50 | assigns(:link).should be_a_new(Link) 51 | end 52 | end 53 | 54 | describe "GET edit" do 55 | it "assigns the requested link as @link" do 56 | link = Link.create! valid_attributes 57 | get :edit, :id => link.id 58 | assigns(:link).should eq(link) 59 | end 60 | end 61 | 62 | describe "POST create" do 63 | describe "with valid params" do 64 | it "creates a new Link" do 65 | expect { 66 | post :create, :link => valid_attributes 67 | }.to change(Link, :count).by(1) 68 | end 69 | 70 | it "assigns a newly created link as @link" do 71 | post :create, :link => valid_attributes 72 | assigns(:link).should be_a(Link) 73 | assigns(:link).should be_persisted 74 | end 75 | 76 | it "redirects to the created link" do 77 | post :create, :link => valid_attributes 78 | response.should redirect_to(Link.last) 79 | end 80 | end 81 | 82 | describe "with invalid params" do 83 | it "assigns a newly created but unsaved link as @link" do 84 | # Trigger the behavior that occurs when invalid params are submitted 85 | Link.any_instance.stub(:save).and_return(false) 86 | post :create, :link => {} 87 | assigns(:link).should be_a_new(Link) 88 | end 89 | 90 | it "re-renders the 'new' template" do 91 | # Trigger the behavior that occurs when invalid params are submitted 92 | Link.any_instance.stub(:save).and_return(false) 93 | post :create, :link => {} 94 | response.should render_template("new") 95 | end 96 | end 97 | end 98 | 99 | describe "PUT update" do 100 | describe "with valid params" do 101 | it "updates the requested link" do 102 | link = Link.create! valid_attributes 103 | # Assuming there are no other links in the database, this 104 | # specifies that the Link created on the previous line 105 | # receives the :update_attributes message with whatever params are 106 | # submitted in the request. 107 | Link.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) 108 | put :update, :id => link.id, :link => {'these' => 'params'} 109 | end 110 | 111 | it "assigns the requested link as @link" do 112 | link = Link.create! valid_attributes 113 | put :update, :id => link.id, :link => valid_attributes 114 | assigns(:link).should eq(link) 115 | end 116 | 117 | it "redirects to the link" do 118 | link = Link.create! valid_attributes 119 | put :update, :id => link.id, :link => valid_attributes 120 | response.should redirect_to(link) 121 | end 122 | end 123 | 124 | describe "with invalid params" do 125 | it "assigns the link as @link" do 126 | link = Link.create! valid_attributes 127 | # Trigger the behavior that occurs when invalid params are submitted 128 | Link.any_instance.stub(:save).and_return(false) 129 | put :update, :id => link.id, :link => {} 130 | assigns(:link).should eq(link) 131 | end 132 | 133 | it "re-renders the 'edit' template" do 134 | link = Link.create! valid_attributes 135 | # Trigger the behavior that occurs when invalid params are submitted 136 | Link.any_instance.stub(:save).and_return(false) 137 | put :update, :id => link.id, :link => {} 138 | response.should render_template("edit") 139 | end 140 | end 141 | end 142 | 143 | describe "DELETE destroy" do 144 | it "destroys the requested link" do 145 | link = Link.create! valid_attributes 146 | expect { 147 | delete :destroy, :id => link.id 148 | }.to change(Link, :count).by(-1) 149 | end 150 | 151 | it "redirects to the links list" do 152 | link = Link.create! valid_attributes 153 | delete :destroy, :id => link.id 154 | response.should redirect_to(links_url) 155 | end 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /spec/controllers/tags_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # This spec was generated by rspec-rails when you ran the scaffold generator. 5 | # It demonstrates how one might use RSpec to specify the controller code that 6 | # was generated by Rails when you ran the scaffold generator. 7 | # 8 | # It assumes that the implementation code is generated by the rails scaffold 9 | # generator. If you are using any extension libraries to generate different 10 | # controller code, this generated spec may or may not pass. 11 | # 12 | # It only uses APIs available in rails and/or rspec-rails. There are a number 13 | # of tools you can use to make these specs even more expressive, but we're 14 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 15 | # 16 | # Compared to earlier versions of this generator, there is very limited use of 17 | # stubs and message expectations in this spec. Stubs are only used when there 18 | # is no simpler way to get a handle on the object needed for the example. 19 | # Message expectations are only used when there is no simpler way to specify 20 | # that an instance is receiving a specific message. 21 | 22 | describe TagsController do 23 | 24 | # This should return the minimal set of attributes required to create a valid 25 | # Tag. As you add validations to Tag, be sure to 26 | # update the return value of this method accordingly. 27 | def valid_attributes 28 | {} 29 | end 30 | 31 | describe "GET index" do 32 | it "assigns all tags as @tags" do 33 | tag = Tag.create! valid_attributes 34 | get :index 35 | assigns(:tags).should eq([tag]) 36 | end 37 | end 38 | 39 | describe "GET show" do 40 | it "assigns the requested tag as @tag" do 41 | tag = Tag.create! valid_attributes 42 | get :show, :id => tag.id 43 | assigns(:tag).should eq(tag) 44 | end 45 | end 46 | 47 | describe "GET new" do 48 | it "assigns a new tag as @tag" do 49 | get :new 50 | assigns(:tag).should be_a_new(Tag) 51 | end 52 | end 53 | 54 | describe "GET edit" do 55 | it "assigns the requested tag as @tag" do 56 | tag = Tag.create! valid_attributes 57 | get :edit, :id => tag.id 58 | assigns(:tag).should eq(tag) 59 | end 60 | end 61 | 62 | describe "POST create" do 63 | describe "with valid params" do 64 | it "creates a new Tag" do 65 | expect { 66 | post :create, :tag => valid_attributes 67 | }.to change(Tag, :count).by(1) 68 | end 69 | 70 | it "assigns a newly created tag as @tag" do 71 | post :create, :tag => valid_attributes 72 | assigns(:tag).should be_a(Tag) 73 | assigns(:tag).should be_persisted 74 | end 75 | 76 | it "redirects to the created tag" do 77 | post :create, :tag => valid_attributes 78 | response.should redirect_to(Tag.last) 79 | end 80 | end 81 | 82 | describe "with invalid params" do 83 | it "assigns a newly created but unsaved tag as @tag" do 84 | # Trigger the behavior that occurs when invalid params are submitted 85 | Tag.any_instance.stub(:save).and_return(false) 86 | post :create, :tag => {} 87 | assigns(:tag).should be_a_new(Tag) 88 | end 89 | 90 | it "re-renders the 'new' template" do 91 | # Trigger the behavior that occurs when invalid params are submitted 92 | Tag.any_instance.stub(:save).and_return(false) 93 | post :create, :tag => {} 94 | response.should render_template("new") 95 | end 96 | end 97 | end 98 | 99 | describe "PUT update" do 100 | describe "with valid params" do 101 | it "updates the requested tag" do 102 | tag = Tag.create! valid_attributes 103 | # Assuming there are no other tags in the database, this 104 | # specifies that the Tag created on the previous line 105 | # receives the :update_attributes message with whatever params are 106 | # submitted in the request. 107 | Tag.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) 108 | put :update, :id => tag.id, :tag => {'these' => 'params'} 109 | end 110 | 111 | it "assigns the requested tag as @tag" do 112 | tag = Tag.create! valid_attributes 113 | put :update, :id => tag.id, :tag => valid_attributes 114 | assigns(:tag).should eq(tag) 115 | end 116 | 117 | it "redirects to the tag" do 118 | tag = Tag.create! valid_attributes 119 | put :update, :id => tag.id, :tag => valid_attributes 120 | response.should redirect_to(tag) 121 | end 122 | end 123 | 124 | describe "with invalid params" do 125 | it "assigns the tag as @tag" do 126 | tag = Tag.create! valid_attributes 127 | # Trigger the behavior that occurs when invalid params are submitted 128 | Tag.any_instance.stub(:save).and_return(false) 129 | put :update, :id => tag.id, :tag => {} 130 | assigns(:tag).should eq(tag) 131 | end 132 | 133 | it "re-renders the 'edit' template" do 134 | tag = Tag.create! valid_attributes 135 | # Trigger the behavior that occurs when invalid params are submitted 136 | Tag.any_instance.stub(:save).and_return(false) 137 | put :update, :id => tag.id, :tag => {} 138 | response.should render_template("edit") 139 | end 140 | end 141 | end 142 | 143 | describe "DELETE destroy" do 144 | it "destroys the requested tag" do 145 | tag = Tag.create! valid_attributes 146 | expect { 147 | delete :destroy, :id => tag.id 148 | }.to change(Tag, :count).by(-1) 149 | end 150 | 151 | it "redirects to the tags list" do 152 | tag = Tag.create! valid_attributes 153 | delete :destroy, :id => tag.id 154 | response.should redirect_to(tags_url) 155 | end 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /spec/controllers/tweets_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # This spec was generated by rspec-rails when you ran the scaffold generator. 5 | # It demonstrates how one might use RSpec to specify the controller code that 6 | # was generated by Rails when you ran the scaffold generator. 7 | # 8 | # It assumes that the implementation code is generated by the rails scaffold 9 | # generator. If you are using any extension libraries to generate different 10 | # controller code, this generated spec may or may not pass. 11 | # 12 | # It only uses APIs available in rails and/or rspec-rails. There are a number 13 | # of tools you can use to make these specs even more expressive, but we're 14 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 15 | # 16 | # Compared to earlier versions of this generator, there is very limited use of 17 | # stubs and message expectations in this spec. Stubs are only used when there 18 | # is no simpler way to get a handle on the object needed for the example. 19 | # Message expectations are only used when there is no simpler way to specify 20 | # that an instance is receiving a specific message. 21 | 22 | describe TweetsController do 23 | 24 | # This should return the minimal set of attributes required to create a valid 25 | # Tweet. As you add validations to Tweet, be sure to 26 | # update the return value of this method accordingly. 27 | def valid_attributes 28 | {} 29 | end 30 | 31 | describe "GET index" do 32 | it "assigns all tweets as @tweets" do 33 | tweet = Tweet.create! valid_attributes 34 | get :index 35 | assigns(:tweets).should eq([tweet]) 36 | end 37 | end 38 | 39 | describe "GET show" do 40 | it "assigns the requested tweet as @tweet" do 41 | tweet = Tweet.create! valid_attributes 42 | get :show, :id => tweet.id 43 | assigns(:tweet).should eq(tweet) 44 | end 45 | end 46 | 47 | describe "GET new" do 48 | it "assigns a new tweet as @tweet" do 49 | get :new 50 | assigns(:tweet).should be_a_new(Tweet) 51 | end 52 | end 53 | 54 | describe "GET edit" do 55 | it "assigns the requested tweet as @tweet" do 56 | tweet = Tweet.create! valid_attributes 57 | get :edit, :id => tweet.id 58 | assigns(:tweet).should eq(tweet) 59 | end 60 | end 61 | 62 | describe "POST create" do 63 | describe "with valid params" do 64 | it "creates a new Tweet" do 65 | expect { 66 | post :create, :tweet => valid_attributes 67 | }.to change(Tweet, :count).by(1) 68 | end 69 | 70 | it "assigns a newly created tweet as @tweet" do 71 | post :create, :tweet => valid_attributes 72 | assigns(:tweet).should be_a(Tweet) 73 | assigns(:tweet).should be_persisted 74 | end 75 | 76 | it "redirects to the created tweet" do 77 | post :create, :tweet => valid_attributes 78 | response.should redirect_to(Tweet.last) 79 | end 80 | end 81 | 82 | describe "with invalid params" do 83 | it "assigns a newly created but unsaved tweet as @tweet" do 84 | # Trigger the behavior that occurs when invalid params are submitted 85 | Tweet.any_instance.stub(:save).and_return(false) 86 | post :create, :tweet => {} 87 | assigns(:tweet).should be_a_new(Tweet) 88 | end 89 | 90 | it "re-renders the 'new' template" do 91 | # Trigger the behavior that occurs when invalid params are submitted 92 | Tweet.any_instance.stub(:save).and_return(false) 93 | post :create, :tweet => {} 94 | response.should render_template("new") 95 | end 96 | end 97 | end 98 | 99 | describe "PUT update" do 100 | describe "with valid params" do 101 | it "updates the requested tweet" do 102 | tweet = Tweet.create! valid_attributes 103 | # Assuming there are no other tweets in the database, this 104 | # specifies that the Tweet created on the previous line 105 | # receives the :update_attributes message with whatever params are 106 | # submitted in the request. 107 | Tweet.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) 108 | put :update, :id => tweet.id, :tweet => {'these' => 'params'} 109 | end 110 | 111 | it "assigns the requested tweet as @tweet" do 112 | tweet = Tweet.create! valid_attributes 113 | put :update, :id => tweet.id, :tweet => valid_attributes 114 | assigns(:tweet).should eq(tweet) 115 | end 116 | 117 | it "redirects to the tweet" do 118 | tweet = Tweet.create! valid_attributes 119 | put :update, :id => tweet.id, :tweet => valid_attributes 120 | response.should redirect_to(tweet) 121 | end 122 | end 123 | 124 | describe "with invalid params" do 125 | it "assigns the tweet as @tweet" do 126 | tweet = Tweet.create! valid_attributes 127 | # Trigger the behavior that occurs when invalid params are submitted 128 | Tweet.any_instance.stub(:save).and_return(false) 129 | put :update, :id => tweet.id, :tweet => {} 130 | assigns(:tweet).should eq(tweet) 131 | end 132 | 133 | it "re-renders the 'edit' template" do 134 | tweet = Tweet.create! valid_attributes 135 | # Trigger the behavior that occurs when invalid params are submitted 136 | Tweet.any_instance.stub(:save).and_return(false) 137 | put :update, :id => tweet.id, :tweet => {} 138 | response.should render_template("edit") 139 | end 140 | end 141 | end 142 | 143 | describe "DELETE destroy" do 144 | it "destroys the requested tweet" do 145 | tweet = Tweet.create! valid_attributes 146 | expect { 147 | delete :destroy, :id => tweet.id 148 | }.to change(Tweet, :count).by(-1) 149 | end 150 | 151 | it "redirects to the tweets list" do 152 | tweet = Tweet.create! valid_attributes 153 | delete :destroy, :id => tweet.id 154 | response.should redirect_to(tweets_url) 155 | end 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # This spec was generated by rspec-rails when you ran the scaffold generator. 5 | # It demonstrates how one might use RSpec to specify the controller code that 6 | # was generated by Rails when you ran the scaffold generator. 7 | # 8 | # It assumes that the implementation code is generated by the rails scaffold 9 | # generator. If you are using any extension libraries to generate different 10 | # controller code, this generated spec may or may not pass. 11 | # 12 | # It only uses APIs available in rails and/or rspec-rails. There are a number 13 | # of tools you can use to make these specs even more expressive, but we're 14 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 15 | # 16 | # Compared to earlier versions of this generator, there is very limited use of 17 | # stubs and message expectations in this spec. Stubs are only used when there 18 | # is no simpler way to get a handle on the object needed for the example. 19 | # Message expectations are only used when there is no simpler way to specify 20 | # that an instance is receiving a specific message. 21 | 22 | describe UsersController do 23 | 24 | # This should return the minimal set of attributes required to create a valid 25 | # User. As you add validations to User, be sure to 26 | # update the return value of this method accordingly. 27 | def valid_attributes 28 | {} 29 | end 30 | 31 | describe "GET index" do 32 | it "assigns all users as @users" do 33 | user = User.create! valid_attributes 34 | get :index 35 | assigns(:users).should eq([user]) 36 | end 37 | end 38 | 39 | describe "GET show" do 40 | it "assigns the requested user as @user" do 41 | user = User.create! valid_attributes 42 | get :show, :id => user.id 43 | assigns(:user).should eq(user) 44 | end 45 | end 46 | 47 | describe "GET new" do 48 | it "assigns a new user as @user" do 49 | get :new 50 | assigns(:user).should be_a_new(User) 51 | end 52 | end 53 | 54 | describe "GET edit" do 55 | it "assigns the requested user as @user" do 56 | user = User.create! valid_attributes 57 | get :edit, :id => user.id 58 | assigns(:user).should eq(user) 59 | end 60 | end 61 | 62 | describe "POST create" do 63 | describe "with valid params" do 64 | it "creates a new User" do 65 | expect { 66 | post :create, :user => valid_attributes 67 | }.to change(User, :count).by(1) 68 | end 69 | 70 | it "assigns a newly created user as @user" do 71 | post :create, :user => valid_attributes 72 | assigns(:user).should be_a(User) 73 | assigns(:user).should be_persisted 74 | end 75 | 76 | it "redirects to the created user" do 77 | post :create, :user => valid_attributes 78 | response.should redirect_to(User.last) 79 | end 80 | end 81 | 82 | describe "with invalid params" do 83 | it "assigns a newly created but unsaved user as @user" do 84 | # Trigger the behavior that occurs when invalid params are submitted 85 | User.any_instance.stub(:save).and_return(false) 86 | post :create, :user => {} 87 | assigns(:user).should be_a_new(User) 88 | end 89 | 90 | it "re-renders the 'new' template" do 91 | # Trigger the behavior that occurs when invalid params are submitted 92 | User.any_instance.stub(:save).and_return(false) 93 | post :create, :user => {} 94 | response.should render_template("new") 95 | end 96 | end 97 | end 98 | 99 | describe "PUT update" do 100 | describe "with valid params" do 101 | it "updates the requested user" do 102 | user = User.create! valid_attributes 103 | # Assuming there are no other users in the database, this 104 | # specifies that the User created on the previous line 105 | # receives the :update_attributes message with whatever params are 106 | # submitted in the request. 107 | User.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) 108 | put :update, :id => user.id, :user => {'these' => 'params'} 109 | end 110 | 111 | it "assigns the requested user as @user" do 112 | user = User.create! valid_attributes 113 | put :update, :id => user.id, :user => valid_attributes 114 | assigns(:user).should eq(user) 115 | end 116 | 117 | it "redirects to the user" do 118 | user = User.create! valid_attributes 119 | put :update, :id => user.id, :user => valid_attributes 120 | response.should redirect_to(user) 121 | end 122 | end 123 | 124 | describe "with invalid params" do 125 | it "assigns the user as @user" do 126 | user = User.create! valid_attributes 127 | # Trigger the behavior that occurs when invalid params are submitted 128 | User.any_instance.stub(:save).and_return(false) 129 | put :update, :id => user.id, :user => {} 130 | assigns(:user).should eq(user) 131 | end 132 | 133 | it "re-renders the 'edit' template" do 134 | user = User.create! valid_attributes 135 | # Trigger the behavior that occurs when invalid params are submitted 136 | User.any_instance.stub(:save).and_return(false) 137 | put :update, :id => user.id, :user => {} 138 | response.should render_template("edit") 139 | end 140 | end 141 | end 142 | 143 | describe "DELETE destroy" do 144 | it "destroys the requested user" do 145 | user = User.create! valid_attributes 146 | expect { 147 | delete :destroy, :id => user.id 148 | }.to change(User, :count).by(-1) 149 | end 150 | 151 | it "redirects to the users list" do 152 | user = User.create! valid_attributes 153 | delete :destroy, :id => user.id 154 | response.should redirect_to(users_url) 155 | end 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /spec/helpers/links_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Specs in this file have access to a helper object that includes 5 | # the LinksHelper. For example: 6 | # 7 | # describe LinksHelper do 8 | # describe "string concat" do 9 | # it "concats two strings with spaces" do 10 | # helper.concat_strings("this","that").should == "this that" 11 | # end 12 | # end 13 | # end 14 | describe LinksHelper do 15 | pending "add some examples to (or delete) #{__FILE__}" 16 | end 17 | -------------------------------------------------------------------------------- /spec/helpers/tags_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Specs in this file have access to a helper object that includes 5 | # the TagsHelper. For example: 6 | # 7 | # describe TagsHelper do 8 | # describe "string concat" do 9 | # it "concats two strings with spaces" do 10 | # helper.concat_strings("this","that").should == "this that" 11 | # end 12 | # end 13 | # end 14 | describe TagsHelper do 15 | pending "add some examples to (or delete) #{__FILE__}" 16 | end 17 | -------------------------------------------------------------------------------- /spec/helpers/tweets_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Specs in this file have access to a helper object that includes 5 | # the TweetsHelper. For example: 6 | # 7 | # describe TweetsHelper do 8 | # describe "string concat" do 9 | # it "concats two strings with spaces" do 10 | # helper.concat_strings("this","that").should == "this that" 11 | # end 12 | # end 13 | # end 14 | describe TweetsHelper do 15 | pending "add some examples to (or delete) #{__FILE__}" 16 | end 17 | -------------------------------------------------------------------------------- /spec/helpers/users_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | # Specs in this file have access to a helper object that includes 5 | # the UsersHelper. For example: 6 | # 7 | # describe UsersHelper do 8 | # describe "string concat" do 9 | # it "concats two strings with spaces" do 10 | # helper.concat_strings("this","that").should == "this that" 11 | # end 12 | # end 13 | # end 14 | describe UsersHelper do 15 | pending "add some examples to (or delete) #{__FILE__}" 16 | end 17 | -------------------------------------------------------------------------------- /spec/models/link_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe Link do 5 | pending "add some examples to (or delete) #{__FILE__}" 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/tag_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe Tag do 5 | pending "add some examples to (or delete) #{__FILE__}" 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/tweet_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe Tweet do 5 | pending "add some examples to (or delete) #{__FILE__}" 6 | end 7 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe User do 5 | pending "add some examples to (or delete) #{__FILE__}" 6 | end 7 | -------------------------------------------------------------------------------- /spec/requests/links_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "Links" do 5 | describe "GET /links" do 6 | it "works! (now write some real specs)" do 7 | # Run the generator again with the --webrat flag if you want to use webrat methods/matchers 8 | get links_path 9 | response.status.should be(200) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/requests/tags_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "Tags" do 5 | describe "GET /tags" do 6 | it "works! (now write some real specs)" do 7 | # Run the generator again with the --webrat flag if you want to use webrat methods/matchers 8 | get tags_path 9 | response.status.should be(200) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/requests/tweets_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "Tweets" do 5 | describe "GET /tweets" do 6 | it "works! (now write some real specs)" do 7 | # Run the generator again with the --webrat flag if you want to use webrat methods/matchers 8 | get tweets_path 9 | response.status.should be(200) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/requests/users_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "Users" do 5 | describe "GET /users" do 6 | it "works! (now write some real specs)" do 7 | # Run the generator again with the --webrat flag if you want to use webrat methods/matchers 8 | get users_path 9 | response.status.should be(200) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/routing/links_routing_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require "spec_helper" 3 | 4 | describe LinksController do 5 | describe "routing" do 6 | 7 | it "routes to #index" do 8 | get("/links").should route_to("links#index") 9 | end 10 | 11 | it "routes to #new" do 12 | get("/links/new").should route_to("links#new") 13 | end 14 | 15 | it "routes to #show" do 16 | get("/links/1").should route_to("links#show", :id => "1") 17 | end 18 | 19 | it "routes to #edit" do 20 | get("/links/1/edit").should route_to("links#edit", :id => "1") 21 | end 22 | 23 | it "routes to #create" do 24 | post("/links").should route_to("links#create") 25 | end 26 | 27 | it "routes to #update" do 28 | put("/links/1").should route_to("links#update", :id => "1") 29 | end 30 | 31 | it "routes to #destroy" do 32 | delete("/links/1").should route_to("links#destroy", :id => "1") 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/routing/tags_routing_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require "spec_helper" 3 | 4 | describe TagsController do 5 | describe "routing" do 6 | 7 | it "routes to #index" do 8 | get("/tags").should route_to("tags#index") 9 | end 10 | 11 | it "routes to #new" do 12 | get("/tags/new").should route_to("tags#new") 13 | end 14 | 15 | it "routes to #show" do 16 | get("/tags/1").should route_to("tags#show", :id => "1") 17 | end 18 | 19 | it "routes to #edit" do 20 | get("/tags/1/edit").should route_to("tags#edit", :id => "1") 21 | end 22 | 23 | it "routes to #create" do 24 | post("/tags").should route_to("tags#create") 25 | end 26 | 27 | it "routes to #update" do 28 | put("/tags/1").should route_to("tags#update", :id => "1") 29 | end 30 | 31 | it "routes to #destroy" do 32 | delete("/tags/1").should route_to("tags#destroy", :id => "1") 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/routing/tweets_routing_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require "spec_helper" 3 | 4 | describe TweetsController do 5 | describe "routing" do 6 | 7 | it "routes to #index" do 8 | get("/tweets").should route_to("tweets#index") 9 | end 10 | 11 | it "routes to #new" do 12 | get("/tweets/new").should route_to("tweets#new") 13 | end 14 | 15 | it "routes to #show" do 16 | get("/tweets/1").should route_to("tweets#show", :id => "1") 17 | end 18 | 19 | it "routes to #edit" do 20 | get("/tweets/1/edit").should route_to("tweets#edit", :id => "1") 21 | end 22 | 23 | it "routes to #create" do 24 | post("/tweets").should route_to("tweets#create") 25 | end 26 | 27 | it "routes to #update" do 28 | put("/tweets/1").should route_to("tweets#update", :id => "1") 29 | end 30 | 31 | it "routes to #destroy" do 32 | delete("/tweets/1").should route_to("tweets#destroy", :id => "1") 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/routing/users_routing_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require "spec_helper" 3 | 4 | describe UsersController do 5 | describe "routing" do 6 | 7 | it "routes to #index" do 8 | get("/users").should route_to("users#index") 9 | end 10 | 11 | it "routes to #new" do 12 | get("/users/new").should route_to("users#new") 13 | end 14 | 15 | it "routes to #show" do 16 | get("/users/1").should route_to("users#show", :id => "1") 17 | end 18 | 19 | it "routes to #edit" do 20 | get("/users/1/edit").should route_to("users#edit", :id => "1") 21 | end 22 | 23 | it "routes to #create" do 24 | post("/users").should route_to("users#create") 25 | end 26 | 27 | it "routes to #update" do 28 | put("/users/1").should route_to("users#update", :id => "1") 29 | end 30 | 31 | it "routes to #destroy" do 32 | delete("/users/1").should route_to("users#destroy", :id => "1") 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/views/links/edit.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "links/edit.html.erb" do 5 | before(:each) do 6 | @link = assign(:link, stub_model(Link, 7 | :url => "MyString" 8 | )) 9 | end 10 | 11 | it "renders the edit link form" do 12 | render 13 | 14 | # Run the generator again with the --webrat flag if you want to use webrat matchers 15 | assert_select "form", :action => links_path(@link), :method => "post" do 16 | assert_select "input#link_url", :name => "link[url]" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/views/links/index.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "links/index.html.erb" do 5 | before(:each) do 6 | assign(:links, [ 7 | stub_model(Link, 8 | :url => "Url" 9 | ), 10 | stub_model(Link, 11 | :url => "Url" 12 | ) 13 | ]) 14 | end 15 | 16 | it "renders a list of links" do 17 | render 18 | # Run the generator again with the --webrat flag if you want to use webrat matchers 19 | assert_select "tr>td", :text => "Url".to_s, :count => 2 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/views/links/new.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "links/new.html.erb" do 5 | before(:each) do 6 | assign(:link, stub_model(Link, 7 | :url => "MyString" 8 | ).as_new_record) 9 | end 10 | 11 | it "renders new link form" do 12 | render 13 | 14 | # Run the generator again with the --webrat flag if you want to use webrat matchers 15 | assert_select "form", :action => links_path, :method => "post" do 16 | assert_select "input#link_url", :name => "link[url]" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/views/links/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "links/show.html.erb" do 5 | before(:each) do 6 | @link = assign(:link, stub_model(Link, 7 | :url => "Url" 8 | )) 9 | end 10 | 11 | it "renders attributes in

" do 12 | render 13 | # Run the generator again with the --webrat flag if you want to use webrat matchers 14 | rendered.should match(/Url/) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/views/tags/edit.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tags/edit.html.erb" do 5 | before(:each) do 6 | @tag = assign(:tag, stub_model(Tag, 7 | :name => "MyString" 8 | )) 9 | end 10 | 11 | it "renders the edit tag form" do 12 | render 13 | 14 | # Run the generator again with the --webrat flag if you want to use webrat matchers 15 | assert_select "form", :action => tags_path(@tag), :method => "post" do 16 | assert_select "input#tag_name", :name => "tag[name]" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/views/tags/index.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tags/index.html.erb" do 5 | before(:each) do 6 | assign(:tags, [ 7 | stub_model(Tag, 8 | :name => "Name" 9 | ), 10 | stub_model(Tag, 11 | :name => "Name" 12 | ) 13 | ]) 14 | end 15 | 16 | it "renders a list of tags" do 17 | render 18 | # Run the generator again with the --webrat flag if you want to use webrat matchers 19 | assert_select "tr>td", :text => "Name".to_s, :count => 2 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/views/tags/new.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tags/new.html.erb" do 5 | before(:each) do 6 | assign(:tag, stub_model(Tag, 7 | :name => "MyString" 8 | ).as_new_record) 9 | end 10 | 11 | it "renders new tag form" do 12 | render 13 | 14 | # Run the generator again with the --webrat flag if you want to use webrat matchers 15 | assert_select "form", :action => tags_path, :method => "post" do 16 | assert_select "input#tag_name", :name => "tag[name]" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/views/tags/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tags/show.html.erb" do 5 | before(:each) do 6 | @tag = assign(:tag, stub_model(Tag, 7 | :name => "Name" 8 | )) 9 | end 10 | 11 | it "renders attributes in

" do 12 | render 13 | # Run the generator again with the --webrat flag if you want to use webrat matchers 14 | rendered.should match(/Name/) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/views/tweets/edit.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tweets/edit.html.erb" do 5 | before(:each) do 6 | @tweet = assign(:tweet, stub_model(Tweet, 7 | :text => "MyString", 8 | :link => "MyString", 9 | :tweet_id => "MyString" 10 | )) 11 | end 12 | 13 | it "renders the edit tweet form" do 14 | render 15 | 16 | # Run the generator again with the --webrat flag if you want to use webrat matchers 17 | assert_select "form", :action => tweets_path(@tweet), :method => "post" do 18 | assert_select "input#tweet_text", :name => "tweet[text]" 19 | assert_select "input#tweet_link", :name => "tweet[link]" 20 | assert_select "input#tweet_tweet_id", :name => "tweet[tweet_id]" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/views/tweets/index.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tweets/index.html.erb" do 5 | before(:each) do 6 | assign(:tweets, [ 7 | stub_model(Tweet, 8 | :text => "Text", 9 | :link => "Link", 10 | :tweet_id => "Tweet" 11 | ), 12 | stub_model(Tweet, 13 | :text => "Text", 14 | :link => "Link", 15 | :tweet_id => "Tweet" 16 | ) 17 | ]) 18 | end 19 | 20 | it "renders a list of tweets" do 21 | render 22 | # Run the generator again with the --webrat flag if you want to use webrat matchers 23 | assert_select "tr>td", :text => "Text".to_s, :count => 2 24 | # Run the generator again with the --webrat flag if you want to use webrat matchers 25 | assert_select "tr>td", :text => "Link".to_s, :count => 2 26 | # Run the generator again with the --webrat flag if you want to use webrat matchers 27 | assert_select "tr>td", :text => "Tweet".to_s, :count => 2 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/views/tweets/new.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tweets/new.html.erb" do 5 | before(:each) do 6 | assign(:tweet, stub_model(Tweet, 7 | :text => "MyString", 8 | :link => "MyString", 9 | :tweet_id => "MyString" 10 | ).as_new_record) 11 | end 12 | 13 | it "renders new tweet form" do 14 | render 15 | 16 | # Run the generator again with the --webrat flag if you want to use webrat matchers 17 | assert_select "form", :action => tweets_path, :method => "post" do 18 | assert_select "input#tweet_text", :name => "tweet[text]" 19 | assert_select "input#tweet_link", :name => "tweet[link]" 20 | assert_select "input#tweet_tweet_id", :name => "tweet[tweet_id]" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/views/tweets/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "tweets/show.html.erb" do 5 | before(:each) do 6 | @tweet = assign(:tweet, stub_model(Tweet, 7 | :text => "Text", 8 | :link => "Link", 9 | :tweet_id => "Tweet" 10 | )) 11 | end 12 | 13 | it "renders attributes in

" do 14 | render 15 | # Run the generator again with the --webrat flag if you want to use webrat matchers 16 | rendered.should match(/Text/) 17 | # Run the generator again with the --webrat flag if you want to use webrat matchers 18 | rendered.should match(/Link/) 19 | # Run the generator again with the --webrat flag if you want to use webrat matchers 20 | rendered.should match(/Tweet/) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/views/users/edit.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "users/edit.html.erb" do 5 | before(:each) do 6 | @user = assign(:user, stub_model(User, 7 | :twid => "MyString", 8 | :link => "MyString" 9 | )) 10 | end 11 | 12 | it "renders the edit user form" do 13 | render 14 | 15 | # Run the generator again with the --webrat flag if you want to use webrat matchers 16 | assert_select "form", :action => users_path(@user), :method => "post" do 17 | assert_select "input#user_twid", :name => "user[twid]" 18 | assert_select "input#user_link", :name => "user[link]" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/views/users/index.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "users/index.html.erb" do 5 | before(:each) do 6 | assign(:users, [ 7 | stub_model(User, 8 | :twid => "Twid", 9 | :link => "Link" 10 | ), 11 | stub_model(User, 12 | :twid => "Twid", 13 | :link => "Link" 14 | ) 15 | ]) 16 | end 17 | 18 | it "renders a list of users" do 19 | render 20 | # Run the generator again with the --webrat flag if you want to use webrat matchers 21 | assert_select "tr>td", :text => "Twid".to_s, :count => 2 22 | # Run the generator again with the --webrat flag if you want to use webrat matchers 23 | assert_select "tr>td", :text => "Link".to_s, :count => 2 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/views/users/new.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "users/new.html.erb" do 5 | before(:each) do 6 | assign(:user, stub_model(User, 7 | :twid => "MyString", 8 | :link => "MyString" 9 | ).as_new_record) 10 | end 11 | 12 | it "renders new user form" do 13 | render 14 | 15 | # Run the generator again with the --webrat flag if you want to use webrat matchers 16 | assert_select "form", :action => users_path, :method => "post" do 17 | assert_select "input#user_twid", :name => "user[twid]" 18 | assert_select "input#user_link", :name => "user[link]" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/views/users/show.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'spec_helper' 3 | 4 | describe "users/show.html.erb" do 5 | before(:each) do 6 | @user = assign(:user, stub_model(User, 7 | :twid => "Twid", 8 | :link => "Link" 9 | )) 10 | end 11 | 12 | it "renders attributes in

" do 13 | render 14 | # Run the generator again with the --webrat flag if you want to use webrat matchers 15 | rendered.should match(/Twid/) 16 | # Run the generator again with the --webrat flag if you want to use webrat matchers 17 | rendered.should match(/Link/) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/test/fixtures/.gitkeep -------------------------------------------------------------------------------- /test/functional/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/test/functional/.gitkeep -------------------------------------------------------------------------------- /test/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/test/integration/.gitkeep -------------------------------------------------------------------------------- /test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'test_helper' 3 | require 'rails/performance_test_help' 4 | 5 | class BrowsingTest < ActionDispatch::PerformanceTest 6 | # Refer to the documentation for all available options 7 | # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] 8 | # :output => 'tmp/performance', :formats => [:flat] } 9 | 10 | def test_homepage 11 | get '/' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | ENV["RAILS_ENV"] = "test" 3 | require File.expand_path('../../config/environment', __FILE__) 4 | require 'rails/test_help' 5 | 6 | class ActiveSupport::TestCase 7 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 8 | # 9 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 10 | # -- they do not yet inherit this setting 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/test/unit/.gitkeep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/vendor/assets/stylesheets/.gitkeep -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4jrb/kvitter/507cd0920b6a8b289f4d896ec8a87a70584cfc83/vendor/plugins/.gitkeep --------------------------------------------------------------------------------