Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Committing the rest of the existing app.

  • Loading branch information...
commit b3396cff31627775fc498e492fe03bc0726305ce 1 parent 4aacd9c
@apike authored
Showing with 55,117 additions and 0 deletions.
  1. +10 −0 Rakefile
  2. +15 −0 app/controllers/application.rb
  3. +63 −0 app/controllers/default_controller.rb
  4. +68 −0 app/controllers/scan_controller.rb
  5. +3 −0  app/helpers/application_helper.rb
  6. +2 −0  app/helpers/home_helper.rb
  7. +2 −0  app/helpers/login_helper.rb
  8. +2 −0  app/helpers/scan_helper.rb
  9. BIN  app/views/.DS_Store
  10. +23 −0 app/views/default/about.html.erb
  11. +8 −0 app/views/default/index.html.erb
  12. +13 −0 app/views/layouts/_choices.erb
  13. +46 −0 app/views/layouts/application.html.erb
  14. +11 −0 app/views/scan/index.html.erb
  15. +30 −0 app/views/scan/single.html.erb
  16. +109 −0 config/boot.rb
  17. +22 −0 config/database.yml
  18. +84 −0 config/environment.rb
  19. +19 −0 config/environments/development.rb
  20. +24 −0 config/environments/production.rb
  21. +22 −0 config/environments/test.rb
  22. +10 −0 config/initializers/inflections.rb
  23. +5 −0 config/initializers/mime_types.rb
  24. +17 −0 config/initializers/new_rails_defaults.rb
  25. +5 −0 config/locales/en.yml
  26. +45 −0 config/routes.rb
  27. BIN  db/development.sqlite3
  28. +16 −0 db/migrate/20091126050405_create_sessions.rb
  29. BIN  db/production.sqlite3
  30. +24 −0 db/schema.rb
  31. +5 −0 doc/README_FOR_APP
  32. +38,097 −0 log/development.log
  33. +1,367 −0 log/production.log
  34. 0  log/server.log
  35. 0  log/test.log
  36. +6,680 −0 profile1.txt
  37. BIN  public/.DS_Store
  38. +31 −0 public/404.html
  39. +30 −0 public/422.html
  40. +33 −0 public/500.html
  41. +10 −0 public/dispatch.cgi
  42. +24 −0 public/dispatch.fcgi
  43. +10 −0 public/dispatch.rb
  44. 0  public/favicon.gif
  45. BIN  public/favicon.ico
  46. BIN  public/images/about_headline.png
  47. BIN  public/images/bird2bird.png
  48. BIN  public/images/heaviest_headline.png
  49. BIN  public/images/icons/count.png
  50. BIN  public/images/icons/favorites.png
  51. BIN  public/images/icons/hashes.png
  52. BIN  public/images/icons/links.png
  53. BIN  public/images/icons/mentions.png
  54. BIN  public/images/icons/meta.png
  55. BIN  public/images/loader.gif
  56. BIN  public/images/rails.gif
  57. BIN  public/images/scan.png
  58. BIN  public/images/spinner.gif
  59. BIN  public/images/test.gif
  60. BIN  public/images/testing.jpg
  61. BIN  public/images/too_many_tweets.png
  62. BIN  public/images/twitter_sign_in.png
  63. BIN  public/images/unladen-square.png
  64. BIN  public/images/unladen_logo.png
  65. +382 −0 public/javascripts/application.js
  66. +963 −0 public/javascripts/controls.js
  67. +973 −0 public/javascripts/dragdrop.js
  68. +1,128 −0 public/javascripts/effects.js
  69. +4,320 −0 public/javascripts/prototype.js
  70. +5 −0 public/robots.txt
  71. +209 −0 public/stylesheets/unladen.css
  72. +4 −0 script/about
  73. +3 −0  script/console
  74. +3 −0  script/dbconsole
  75. +3 −0  script/destroy
  76. +3 −0  script/generate
  77. +3 −0  script/performance/benchmarker
  78. +3 −0  script/performance/profiler
  79. +3 −0  script/performance/request
  80. +3 −0  script/plugin
  81. +3 −0  script/process/inspector
  82. +3 −0  script/process/reaper
  83. +3 −0  script/process/spawner
  84. +3 −0  script/runner
  85. +3 −0  script/server
  86. +8 −0 test/functional/home_controller_test.rb
  87. +8 −0 test/functional/login_controller_test.rb
  88. +8 −0 test/functional/scan_controller_test.rb
  89. +9 −0 test/performance/browsing_test.rb
  90. +38 −0 test/test_helper.rb
  91. 0  tmp/restart.txt
  92. +38 −0 todo.txt
View
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
View
15 app/controllers/application.rb
@@ -0,0 +1,15 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery :secret => 'f1eaced6b43b7d18fdf7998e1264b7ed'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ # filter_parameter_logging :password
+end
View
63 app/controllers/default_controller.rb
@@ -0,0 +1,63 @@
+class DefaultController < ApplicationController
+ def index
+ end
+
+ def login
+ require 'oauth'
+
+ consumer = get_consumer
+
+ # Get a request token from Twitter
+ begin
+ @request_token = consumer.get_request_token :oauth_callback => ('http://' + request.env['HTTP_HOST'] + '/default/oauth/')
+ rescue
+ render :text => "<p>Twitter couldn't give us an OAuth request token right now. Please try again later.</p><p>" + $!
+ return
+ end
+
+ # Store the request token's details for later
+ session[:request_token] = @request_token.token
+ session[:request_secret] = @request_token.secret
+
+ # Hand off to Twitter so the user can authorize us
+ redirect_to @request_token.authorize_url
+ end
+
+ def oauth
+ require 'oauth'
+ consumer = get_consumer
+
+ # Re-create the request token
+ @request_token = OAuth::RequestToken.new(consumer, session[:request_token], session[:request_secret])
+
+ # Convert the request token to an access token using the verifier Twitter gave us
+ begin
+ @access_token = @request_token.get_access_token(:oauth_verifier => params[:oauth_verifier])
+ rescue
+ render :text => "<p>Twitter couldn't verify your OAuth request token right now. Please try again later.</p><p>" + $!
+ return
+ end
+
+ # Store the token and secret that we need to make API calls
+ session[:oauth_token] = @access_token.token
+ session[:oauth_secret] = @access_token.secret
+
+ redirect_to '/scan/'
+ end
+
+ def logout
+ reset_session
+ redirect_to "/"
+ end
+
+ private
+
+ def get_consumer
+ OAuth::Consumer.new(
+ 'kU9aUc0zXGTRMd1oyknjyg',
+ 'LgLlHSCN27Rs5fTwAoxEFUc2MFAp3VGAdwjaGsRVws',
+ {:site => 'http://twitter.com'}
+ )
+ end
+
+end
View
68 app/controllers/scan_controller.rb
@@ -0,0 +1,68 @@
+require 'grackle'
+require 'date'
+
+class ScanController < ApplicationController
+ def index
+ # Default scanner
+ end
+
+ def single
+ # User page
+ @user = params[:id]
+ end
+
+ def followed
+ # Everybody JSON
+ twitter_request("/statuses/home_timeline.json", true)
+ end
+
+ def user
+ # Call for user JSON
+
+ user = params[:u]
+
+ # Test for invalid twitter username
+ if (!user || user =~ /[^a-z0-9_]+/i)
+ render :text => "{twitter_error: 'Invalid username.'}"
+ else
+ twitter_request("/statuses/user_timeline/" + user + ".json", false)
+ end
+
+ end
+
+ private
+
+ def twitter_request(method, needs_auth)
+ page_size = 200
+ timeout = 20
+
+ page = params[:page] ? params[:page].to_i : 1
+ if page > 10
+ page = 10
+ end
+
+ begin
+ req = Net::HTTP::Get.new(method + "?count=#{page_size}&page=#{page}")
+
+ if (needs_auth)
+ consumer = OAuth::Consumer.new('kU9aUc0zXGTRMd1oyknjyg', 'LgLlHSCN27Rs5fTwAoxEFUc2MFAp3VGAdwjaGsRVws', {:site => 'http://twitter.com'})
+ access_token = OAuth::AccessToken.new(consumer, session[:oauth_token], session[:oauth_secret])
+ consumer.sign!(req, access_token)
+ end
+
+ res = Net::HTTP.new('twitter.com').start {|http|
+ http.read_timeout = timeout
+ http.request(req)
+ }
+ rescue => e
+ res = {}
+ res.code = "Network error - likely a timeout."
+ end
+
+ if res.code.to_i == 200
+ render :text => res.body
+ else
+ render :text => "{twitter_error: #{res.code}}"
+ end
+ end
+end
View
3  app/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
View
2  app/helpers/home_helper.rb
@@ -0,0 +1,2 @@
+module HomeHelper
+end
View
2  app/helpers/login_helper.rb
@@ -0,0 +1,2 @@
+module LoginHelper
+end
View
2  app/helpers/scan_helper.rb
@@ -0,0 +1,2 @@
+module ScanHelper
+end
View
BIN  app/views/.DS_Store
Binary file not shown
View
23 app/views/default/about.html.erb
@@ -0,0 +1,23 @@
+<h1><%= image_tag "about_headline.png" %></h1>
+
+<h2>Why?</h2>
+<p>Unladen Follow is based on the assumption that you actually read your tweets. If catching up on Twitter is taking a lot of time, or you're not able to catch up at all, unfollowing a couple select noisy people can make a huge difference.</p>
+
+<h2>How does it work?</h2>
+<p>Unladen Follow reads your tweet timeline, and analyzes each user's tweet frequency and content. It looks to measure cognitive load: how much time and thought it takes to read those tweets.</p>
+
+<h2>What is a Weekly Tweet Load Unit?</h2>
+<p>Each person you follow is rated in highly scientific Weekly Tweet Load Units (TLU). Your total TLUs are an indicator of how much noise you consume on Twitter every week, based on links, replies, hashtags, talking about Twitter itself, and simple tweet volume. If you favourite a tweet, it reduces the writer's TLU. Very roughly, a TLU takes one second to process, meaning consuming 1661 TLU costs you one day per year of reading Twitter.</p>
+
+<h2>Who is Robert Scoble?</h2>
+<p>Robert Scoble is a tech celebrity who posts a ridiculous amount of stuff on Twitter. Inspired by <a href='http://followcost.com/about/milliscoble'>Follow Cost's metric of milliscobles</a>, Unladen Follow relates your Tweet Load in terms of how many times over you could follow Scoble (which presumably you would not want to do.) On November 29, 2009, using 4 weeks of data, @scobleizer was averaging 250 TLU, which is used in this calculation.</p>
+
+<h2>Why do the numbers in the table not show what I expect?</h2>
+<p>The numbers in the details table for links, hashtags, etc. are not simple quantities. They have been weighted, normalized to one week, and then rounded to the nearest whole value.</p>
+
+
+<h2>What inspired this?</h2>
+<p>The first seed was planted by "Twitter Rank" sites that rank you based on how many @mentions, retweets, and other noisy activities you engaged in. I found myself wanting the opposite of these. The second seed was <a href='http://www.followcost.com/'>Follow Cost</a>, which I liked, but thought could use improved metrics and the ability to apply to my existing followers.
+
+<h2>How can I contact you?</h2>
+<p>I'd be happy to hear from you! You can tweet @apike, or you can use <a href='http://www.antipode.ca/contact/'>this contact form</a>.</p>
View
8 app/views/default/index.html.erb
@@ -0,0 +1,8 @@
+<h1><%= image_tag "too_many_tweets.png" %></h1>
+<p>Cut the noise so you can appreciate the signal.</p>
+<p>Unladen Follow shows you whose tweets are clogging your timeline.</p>
+
+
+<p id='birds'><%= image_tag "bird2bird.png" %></p>
+
+<%= render :partial => 'layouts/choices' %>
View
13 app/views/layouts/_choices.erb
@@ -0,0 +1,13 @@
+<div id='signin'>
+ <% if (session[:oauth_token]) %>
+ <a style='float: right' href='/scan/'>Analyze Now</a>
+ <% else %>
+ <a style='float: right' href='/default/login/'><%= image_tag "twitter_sign_in.png" %></a>
+ <% end %>
+
+ Analyze your followees:
+</div>
+
+<p style='text-align: center; margin: 5px'><i>~or~</i></p>
+
+<div id='signin'><form style='display: inline;float: right' onsubmit='return Unladen.user_jump();'><input type='text' id='jumpto' ></form>Analyze one Twitterer: </div>
View
46 app/views/layouts/application.html.erb
@@ -0,0 +1,46 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
+ <title>Unladen Follow - Twitter analyzer and unfollow helper</title>
+ <%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js" %>
+ <%= javascript_include_tag "application.js" %>
+ <meta name="viewport" content="width=550, user-scalable=yes">
+ <%= stylesheet_link_tag("unladen") %>
+</head>
+<body>
+
+<div id='spacing'>
+<div id='page'>
+
+ <div id='header'>
+ <a href='/'><%= image_tag "unladen_logo.png" %></a>
+ </div>
+
+ <div id='content'>
+ <%= yield %>
+ </div>
+
+ <div id='footer'>
+
+ <a href='/about/'>Learn More</a> - <a href='/scan/'>Analyze</a><br>
+ Brought to you by <a href='http://www.twitter.com/apike/'>@apike</a> of <a href='http://www.antipode.ca/'>Antipode</a>.
+ </div>
+
+</div>
+</div>
+
+<script type="text/javascript">
+var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+try {
+var pageTracker = _gat._getTracker("UA-585700-3");
+pageTracker._trackPageview();
+} catch(err) {}</script>
+
+</body>
+</html>
View
11 app/views/scan/index.html.erb
@@ -0,0 +1,11 @@
+<h1><%= image_tag "heaviest_headline.png" %></h1>
+
+<div id='results'>
+ <span id='progress'></span>
+ <%= image_tag "loader.gif", :id => "loader" %>
+</div>
+
+
+<script>
+ Unladen.get_tweets();
+</script>
View
30 app/views/scan/single.html.erb
@@ -0,0 +1,30 @@
+<% @user = strip_tags @user %>
+
+<h2>Tweet Load for <%= @user %></h2>
+
+<div id='results'>
+ <div id='tlu'><%= image_tag "loader.gif", :id => "loader" %></div>
+
+ <div style='float: right; text-align: right'>
+ <img id='avatar' src=''><br>
+ <a id='link' href='#'></a>
+ </div>
+
+ <table id='single_results'>
+ <tr><td><%= image_tag "icons/count.png" %></td><td>Quantity</td><td id='volume'>-</td></tr>
+ <tr><td><%= image_tag "icons/hashes.png" %></td><td>Hashtags</td><td id='hashes'>-</td></tr>
+ <tr><td><%= image_tag "icons/links.png" %></td><td>Links</td><td id='links'>-</td></tr>
+ <tr><td><%= image_tag "icons/mentions.png" %></td><td>Mentions</td><td id='mentions'>-</td></tr>
+ <tr><td><%= image_tag "icons/meta.png" %></td><td>Metadiscussion</td><td id='meta'>-</td></tr>
+ </table>
+
+ <p id='weeks'><br></p>
+</div>
+
+<br><br>
+
+<%= render :partial => 'layouts/choices' %>
+
+<script>
+ Unladen.get_user("<%= @user %>");
+</script>
View
109 config/boot.rb
@@ -0,0 +1,109 @@
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+ class << self
+ def boot!
+ unless booted?
+ preinitialize
+ pick_boot.run
+ end
+ end
+
+ def booted?
+ defined? Rails::Initializer
+ end
+
+ def pick_boot
+ (vendor_rails? ? VendorBoot : GemBoot).new
+ end
+
+ def vendor_rails?
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
+ end
+
+ def preinitialize
+ load(preinitializer_path) if File.exist?(preinitializer_path)
+ end
+
+ def preinitializer_path
+ "#{RAILS_ROOT}/config/preinitializer.rb"
+ end
+ end
+
+ class Boot
+ def run
+ load_initializer
+ Rails::Initializer.run(:set_load_path)
+ end
+ end
+
+ class VendorBoot < Boot
+ def load_initializer
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+ Rails::Initializer.run(:install_gem_spec_stubs)
+ end
+ end
+
+ class GemBoot < Boot
+ def load_initializer
+ self.class.load_rubygems
+ load_rails_gem
+ require 'initializer'
+ end
+
+ def load_rails_gem
+ if version = self.class.gem_version
+ gem 'rails', version
+ else
+ gem 'rails'
+ end
+ rescue Gem::LoadError => load_error
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+ exit 1
+ end
+
+ class << self
+ def rubygems_version
+ Gem::RubyGemsVersion rescue nil
+ end
+
+ def gem_version
+ if defined? RAILS_GEM_VERSION
+ RAILS_GEM_VERSION
+ elsif ENV.include?('RAILS_GEM_VERSION')
+ ENV['RAILS_GEM_VERSION']
+ else
+ parse_gem_version(read_environment_rb)
+ end
+ end
+
+ def load_rubygems
+ require 'rubygems'
+ min_version = '1.3.1'
+ unless rubygems_version >= min_version
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+ exit 1
+ end
+
+ rescue LoadError
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+ exit 1
+ end
+
+ def parse_gem_version(text)
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+ end
+
+ private
+ def read_environment_rb
+ File.read("#{RAILS_ROOT}/config/environment.rb")
+ end
+ end
+ end
+end
+
+# All that for this:
+Rails.boot!
View
22 config/database.yml
@@ -0,0 +1,22 @@
+# SQLite version 3.x
+# gem install sqlite3-ruby (not necessary on OS X Leopard)
+development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ pool: 5
+ timeout: 5000
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: sqlite3
+ database: db/production.sqlite3
+ pool: 5
+ timeout: 5000
View
84 config/environment.rb
@@ -0,0 +1,84 @@
+# Be sure to restart your server when you modify this file
+
+# Uncomment below to force Rails into production mode when
+# you don't control web/app server and can't set it the proper way
+# ENV['RAILS_ENV'] ||= 'production'
+
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+ # See Rails::Configuration for more options.
+
+ # Skip frameworks you're not going to use. To use Rails without a database
+ # you must remove the Active Record framework.
+ # config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
+
+ # Specify gems that this application depends on.
+ # They can then be installed with "rake gems:install" on new installations.
+ # You have to specify the :lib option for libraries, where the Gem name (sqlite3-ruby) differs from the file itself (sqlite3)
+ # config.gem "bj"
+ # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+ # config.gem "sqlite3-ruby", :lib => "sqlite3"
+ # config.gem "aws-s3", :lib => "aws/s3"
+
+ # Only load the plugins named here, in the order given. By default, all plugins
+ # in vendor/plugins are loaded in alphabetical order.
+ # :all can be used as a placeholder for all plugins not explicitly named
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+
+ # Add additional load paths for your own custom dirs
+ # config.load_paths += %W( #{RAILS_ROOT}/extras )
+
+ # Force all environments to use the same logger level
+ # (by default production uses :info, the others :debug)
+ # config.log_level = :debug
+
+ # Make Time.zone default to the specified zone, and make Active Record store time values
+ # in the database in UTC, and return them converted to the specified local zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time.
+ config.time_zone = 'UTC'
+
+ # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths.
+ # All files from config/locales/*.rb,yml are added automatically.
+ # config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'my', 'locales', '*.{rb,yml}')]
+ # config.i18n.default_locale = :de
+
+ # Your secret key for verifying cookie session data integrity.
+ # If you change this key, all old sessions will become invalid!
+ # Make sure the secret is at least 30 characters and all random,
+ # no regular words or you'll be exposed to dictionary attacks.
+ config.action_controller.session = {
+ :session_key => '_unladen_session',
+ :secret => 'c1be0fcbd0b88622dbc3b6b36a30bc98199629528537388118581f0c0721db253d98e333e38f3e5abbd2f140625dc9c47c5649e2d2741664b6935e5b30fed390'
+ }
+
+ # Use the database for sessions instead of the cookie-based default,
+ # which shouldn't be used to store highly confidential information
+ # (create the session table with "rake db:sessions:create")
+ config.action_controller.session_store = :active_record_store
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Activate observers that should always be running
+ # Please note that observers generated using script/generate observer need to have an _observer suffix
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+
+
+ require 'oauth'
+
+ class OAuth::Consumer
+ def marshal_load(*args)
+ self
+ end
+ end
+end
View
19 config/environments/development.rb
@@ -0,0 +1,19 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# In the development environment your application's code is reloaded on
+# every request. This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+config.cache_classes = false
+
+ActiveSupport::Dependencies.mechanism = :require
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_view.debug_rjs = true
+config.action_controller.perform_caching = false
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false
View
24 config/environments/production.rb
@@ -0,0 +1,24 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Enable threaded mode
+# config.threadsafe!
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Use a different cache store in production
+# config.cache_store = :mem_cache_store
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable delivery errors, bad email addresses will be ignored
+# config.action_mailer.raise_delivery_errors = false
View
22 config/environments/test.rb
@@ -0,0 +1,22 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+# Disable request forgery protection in test environment
+config.action_controller.allow_forgery_protection = false
+
+# Tell Action Mailer not to deliver emails to the real world.
+# The :test delivery method accumulates sent emails in the
+# ActionMailer::Base.deliveries array.
+config.action_mailer.delivery_method = :test
View
10 config/initializers/inflections.rb
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
View
5 config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
+# Mime::Type.register_alias "text/html", :iphone
View
17 config/initializers/new_rails_defaults.rb
@@ -0,0 +1,17 @@
+# These settings change the behavior of Rails 2 apps and will be defaults
+# for Rails 3. You can remove this initializer when Rails 3 is released.
+
+if defined?(ActiveRecord)
+ # Include Active Record class name as root for JSON serialized output.
+ ActiveRecord::Base.include_root_in_json = true
+
+ # Store the full class name (including module namespace) in STI type column.
+ ActiveRecord::Base.store_full_sti_class = true
+end
+
+# Use ISO 8601 format for JSON serialized times and dates.
+ActiveSupport.use_standard_json_time_format = true
+
+# Don't escape HTML entities in JSON, leave that for the #json_escape helper.
+# if you're including raw json in an HTML page.
+ActiveSupport.escape_html_entities_in_json = false
View
5 config/locales/en.yml
@@ -0,0 +1,5 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ hello: "Hello world"
View
45 config/routes.rb
@@ -0,0 +1,45 @@
+ActionController::Routing::Routes.draw do |map|
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Sample of regular route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ map.connect 'about', :controller => 'default', :action => 'about'
+ map.connect 'u/:id', :controller => 'scan', :action => 'single'
+
+
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
+ # map.resources :products
+
+ # Sample resource route with options:
+ # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
+
+ # Sample resource route with sub-resources:
+ # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
+
+ # Sample resource route with more complex sub-resources
+ # map.resources :products do |products|
+ # products.resources :comments
+ # products.resources :sales, :collection => { :recent => :get }
+ # end
+
+ # Sample resource route within a namespace:
+ # map.namespace :admin do |admin|
+ # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
+ # admin.resources :products
+ # end
+
+ # You can have the root of your site routed with map.root -- just remember to delete public/index.html.
+ # map.root :controller => "welcome"
+
+ # See how all your routes lay out with "rake routes"
+
+ # Install the default routes as the lowest priority.
+ # Note: These default routes make all actions in every controller accessible via GET requests. You should
+ # consider removing the them or commenting them out if you're using named routes and resources.
+ map.root :controller => "default"
+
+ map.connect ':controller/:action/:id'
+ map.connect ':controller/:action/:id.:format'
+end
View
BIN  db/development.sqlite3
Binary file not shown
View
16 db/migrate/20091126050405_create_sessions.rb
@@ -0,0 +1,16 @@
+class CreateSessions < ActiveRecord::Migration
+ def self.up
+ create_table :sessions do |t|
+ t.string :session_id, :null => false
+ t.text :data
+ t.timestamps
+ end
+
+ add_index :sessions, :session_id
+ add_index :sessions, :updated_at
+ end
+
+ def self.down
+ drop_table :sessions
+ end
+end
View
BIN  db/production.sqlite3
Binary file not shown
View
24 db/schema.rb
@@ -0,0 +1,24 @@
+# This file is auto-generated from the current state of the database. Instead of editing this file,
+# please use the migrations feature of Active Record to incrementally modify your database, and
+# then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your database schema. If you need
+# to create the application database on another system, you should be using db:schema:load, not running
+# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended to check this file into your version control system.
+
+ActiveRecord::Schema.define(:version => 20091126050405) do
+
+ create_table "sessions", :force => true do |t|
+ t.string "session_id", :null => false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id"
+ add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at"
+
+end
View
5 doc/README_FOR_APP
@@ -0,0 +1,5 @@
+To build the guides:
+
+* Install source-highlighter (http://www.gnu.org/software/src-highlite/source-highlight.html)
+* Install the mizuho gem (http://github.com/FooBarWidget/mizuho/tree/master)
+* Run `rake guides` from the railties directory
View
38,097 log/development.log
38,097 additions, 0 deletions not shown
View
1,367 log/production.log
1,367 additions, 0 deletions not shown
View
0  log/server.log
No changes.
View
0  log/test.log
No changes.
View
6,680 profile1.txt
6,680 additions, 0 deletions not shown
View
BIN  public/.DS_Store
Binary file not shown
View
31 public/404.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>The page you were looking for doesn't exist (404)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #e56532; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/404.html -->
+
+ <div class="dialog">
+ <h1>404'd!</h1>
+ <p>As with most possible URLs on this domain, this one goes nowhere. Your best bet is to <a href='/'>start over at square one</a>.</p>
+ </div>
+</body>
+</html>
View
30 public/422.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>The change you wanted was rejected (422)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+</body>
+</html>
View
33 public/500.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>We're sorry, but something went wrong (500)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #e56532; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <h1>D'oh!</h1>
+ <p>This page had an accident. I'm probably aware, but feel free to <a href='http://www.antipode.ca/contact/'>drop me a line</a> to let me know what's up.</p>
+ <p><small>(If you're the administrator of this website, then please read
+ the log file "<%=h RAILS_ENV %>.log"
+ to find out what went wrong.)</small></p>
+ </div>
+</body>
+</html>
View
10 public/dispatch.cgi
@@ -0,0 +1,10 @@
+#!/usr/bin/ruby1.8
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch
View
24 public/dispatch.fcgi
@@ -0,0 +1,24 @@
+#!/usr/bin/ruby1.8
+#
+# You may specify the path to the FastCGI crash log (a log of unhandled
+# exceptions which forced the FastCGI instance to exit, great for debugging)
+# and the number of requests to process before running garbage collection.
+#
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
+# and the GC period is nil (turned off). A reasonable number of requests
+# could range from 10-100 depending on the memory footprint of your app.
+#
+# Example:
+# # Default log path, normal GC behavior.
+# RailsFCGIHandler.process!
+#
+# # Default log path, 50 requests between GC.
+# RailsFCGIHandler.process! nil, 50
+#
+# # Custom log path, normal GC behavior.
+# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
+#
+require File.dirname(__FILE__) + "/../config/environment"
+require 'fcgi_handler'
+
+RailsFCGIHandler.process!
View
10 public/dispatch.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/ruby1.8
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch
View
0  public/favicon.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/favicon.ico
Binary file not shown
View
BIN  public/images/about_headline.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/bird2bird.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/heaviest_headline.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/count.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/favorites.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/hashes.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/links.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/mentions.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/icons/meta.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/loader.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/rails.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/scan.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/spinner.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/test.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/testing.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/too_many_tweets.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/twitter_sign_in.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/unladen-square.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  public/images/unladen_logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
382 public/javascripts/application.js
@@ -0,0 +1,382 @@
+// Copyright Allen Pike.
+//
+
+
+var Unladen = {
+
+ // Constants
+ CONTACT_STRING: "<p>Please try again, or you can <a href='http://www.antipode.ca/contact/'>contact me</a>.</p>",
+ METRICS: {
+ "count": 1,
+ "favorited" : -5,
+ "hashes": 1,
+ "links": 2,
+ "mentions": 0.5,
+ "meta": 2
+ },
+ MAX_RUNTIME: 6000,
+ MAX_PAGES: 5,
+ PROGRESS_MESSAGES: [
+ "Steeping",
+ "Tweeting",
+ "Snorgling",
+ "Reconstituting",
+ "Interpolating",
+ "Synergizing",
+ "Reticulating",
+ "Digesting",
+ "Regressing",
+ "Crunching",
+ "Lambasting",
+ "Consuming",
+ "Massaging",
+ "Sublimating",
+ "Exercising",
+ "Exorcising",
+ "Divining",
+ "Extrapolating",
+ "Volumetrizing"
+ ],
+
+ // Class variables
+ used_messages: {},
+ simulated_users: {},
+ users: {},
+ oldest_result: null,
+ start_time: new Date(),
+ current_page: 1,
+ initial_weeks_of_data: null,
+
+ // Return false if we've taken too long or have all the data
+ should_keep_going: function() {
+ return (new Date() - this.start_time < this.MAX_RUNTIME && this.current_page < this.MAX_PAGES);
+ },
+
+ // Let's go for an MVC approach for version 2.
+ display_output: function(html) {
+ $('#results').html(html);
+ },
+
+ // Get a message we haven't used before
+ add_progress_message: function() {
+ var m = null;
+ while (!m || this.used_messages[m]) {
+ m = Math.floor(Math.random(new Date().valueOf()) * this.PROGRESS_MESSAGES.length);
+ }
+ this.used_messages[m] = true;
+
+ $('#progress').append('<br>' + this.PROGRESS_MESSAGES[m] + " tweets...");
+ },
+
+ // Hit the server for more tweets
+ get_tweets: function() {
+ this.add_progress_message();
+
+ $("body").ajaxError(function(event, request, settings){
+ console.log(request);
+ Unladen.display_output("Unladen Follow fell down!" + Unladen.CONTACT_STRING + "<p>Error " + request.status + ".");
+ return false;
+ });
+
+ $.getJSON("/scan/followed/?page=" + this.current_page, {}, function(json) {
+ var status = Unladen.process_tweets(json);
+ if (status !== true) {
+ // Here, we could keep going depending on which error it is.
+ Unladen.load_error(status);
+ } else if (Unladen.should_keep_going()) {
+ // Still have more worth querying
+ Unladen.current_page += 1;
+ Unladen.get_tweets();
+ } else {
+ Unladen.process_users();
+ }
+ });
+ },
+
+ // Sort out some raw tweet JSON.
+ process_tweets: function(tweets) {
+ if (tweets.twitter_error) {
+ return tweets.twitter_error;
+ } else if (tweets.length == 0 && !Unladen.users) {
+ return -1;
+ }
+
+ var tweet = {};
+ var users = Unladen.users;
+
+ for (var i = 0; i < tweets.length; i++) {
+ tweet = tweets[i];
+
+ if (tweet.text.match(/^@/) != null) {
+ // Completely ignore @replies
+ continue;
+ }
+
+ name = tweet.user.screen_name.toLowerCase();
+
+ current_user = users[name];
+ if (!current_user) {
+ users[name] = {};
+ current_user = users[name];
+ this.reset_user(current_user);
+ }
+
+ if (!current_user["image"]) {
+ current_user["image"] = tweet.user.profile_image_url;
+ current_user["real_name"] = tweet.user.name;
+ }
+
+ current_user["count"] += 1;
+ current_user["favorited"] += tweet.favorited ? 1 : 0;
+ current_user["hashes"] += this.count_occurrences(tweet.text, '#');
+ current_user["links"] += this.count_occurrences(tweet.text, 'http://');
+ current_user["mentions"] += this.count_occurrences(tweet.text, '@');
+
+ current_user["meta"] += this.count_occurrences(tweet.text, 'twitter');
+ current_user["meta"] += this.count_occurrences(tweet.text, 'media');
+ current_user["meta"] += this.count_occurrences(tweet.text, 'tweet');
+ current_user["meta"] += this.count_occurrences(tweet.text, 'please');
+ current_user["meta"] += this.count_occurrences(tweet.text, 'retweet');
+
+ this.oldest_result = tweet.created_at;
+ }
+
+ return true;
+ },
+
+ // Convert crazy Twitter date format into something parseable.
+ parse_twitter_date: function(string) {
+ //Got: Wed Nov 18 18:36:34 +0000 2009
+ // 0 1 2 3 4 5
+
+ var p = string.split(" ");
+
+ //Convert to: Nov 18 2009 18:36:34 +0000
+ // 1 2 5 3 4
+
+ var order = [1, 2, 5, 3, 4];
+ var parseable = [];
+
+ for (i in order) {
+ parseable.push(p[order[i]]);
+ }
+
+ parseable = parseable.join(" ");
+
+ return new Date(parseable);
+ },
+
+ // Process users for final consumption, now that all JSON calls have been made.
+ process_users: function() {
+ // How many weeks our oldest result was ago
+ var elapsed_milliseconds = new Date().valueOf() - this.parse_twitter_date(this.oldest_result).valueOf();
+ var weeks_of_data = elapsed_milliseconds / 1000 / 60 / 60 / 24 / 7;
+
+ if (!this.initial_weeks_of_data) {
+ // Only store WOD for all-users run
+ this.initial_weeks_of_data = weeks_of_data;
+ }
+
+ var user_array = [];
+ var user = null;
+ var total_cost = 0;
+
+ for (var username in this.users) {
+ user = this.users[username];
+
+ if (!user) {
+ // This can happen if one user fails to load.
+ continue;
+ }
+
+ user_array.push(user);
+
+ if (!user["score"]) {
+ // They weren't processed on a previous round
+ user["name"] = username;
+ user["score"] = 0;
+
+ // Loop through metrics for this user, and adjust for timespan and weights
+ for (var key in this.METRICS) {
+ user[key] /= weeks_of_data;
+ user["score"] += user[key] * this.METRICS[key];
+ }
+ }
+
+ total_cost += user["score"];
+ }
+
+ if (user_array.length == 1) {
+ this.display_single(user_array[0], weeks_of_data);
+ } else {
+ // Sort by score
+ user_array.sort(function(a, b) {
+ return b["score"] - a["score"];
+ });
+
+ this.display_full_table(user_array, total_cost);
+ }
+ },
+
+ display_single: function(user, weeks_of_data) {
+ $('#tlu').html(this.r10(user["score"]));
+ $('#avatar').attr("src", user["image"]);
+ $('#link').attr("href", "http://www.twitter.com/" + user["name"]);
+ $('#link').html(user["real_name"] || user["name"]);
+
+ $("#volume").html(this.r10(user["count"]));
+ $("#hashes").html(this.r10(user["hashes"]));
+ $("#links").html(this.r10(user["links"]));
+ $("#mentions").html(this.r10(user["mentions"]));
+ $("#meta").html(this.r10(user["meta"]));
+
+ $('#weeks').html("Derived from <b>" + this.r10(weeks_of_data) + "</b> weeks of tweets.");
+ },
+
+ display_full_table: function(user_array, total_cost) {
+ var html = [];
+
+ html.push('You are laden with <b>' + Math.round(total_cost) + '</b> Tweet Load Units per week.<br>\
+ This is equivalent to following Robert Scoble <b>' + Math.round(total_cost / 250) + '</b> times.<br>\
+ <a href="/about/">Learn more about Unladen Follow</a>.<br>\
+ \
+ <form id="simulate_form" onsubmit="return Unladen.go_simulate();">\
+ What if I followed <input id="simulate"> ? <img src="/images/loader.gif" id="simulate_spinner">\
+ <p id="simulate_error"></p>\
+ </form>\
+ \
+ <table id="scan">\
+ <tr class="header">\
+ <td>TLU <div>Total Tweet Load Units per Week</div></td>\
+ <td></td>\
+ <td>*<div>Tweet Quantity</div></td>\
+ <td>@<div>@ Mentions</div></td>\
+ <td>#<div>Hash Tags</div></td>\
+ <td>&#8734;<div>Talking About Twitter</div></td>\
+ <td>://<div>Links</div></td>\
+ <td>&#9733;<div>Favourited</div></td>\
+ </tr>');
+
+
+ html.column = function(value, className) {
+ this.push('<td ' + (className ? ('class=' + className) : '') + '>' + Math.round(value) + '</td>');
+ };
+
+ for (key in user_array) {
+ user = user_array[key];
+
+ var class_part = '';
+ if (Unladen.simulated_users[user["name"]]) {
+ class_part = ' class="simulated"';
+ } else {
+ class_part = ' class="qqq"';
+ }
+
+ html.push('<tr' + class_part + '>');
+ html.column(user["score"], 'score');
+ html.push('<td class="user"><a href="/u/' + user["name"] + '">' + user["name"] + '</td>');
+ html.column(user["count"]);
+ html.column(user["mentions"]);
+ html.column(user["hashes"]);
+ html.column(user["meta"]);
+ html.column(user["links"]);
+ html.column(user["favorited"]);
+ html.push('</tr>');
+
+ }
+
+ html.push('</table>\
+ Table derived from <b>' + this.r10(this.initial_weeks_of_data) + '</b> weeks of data.<br>\
+ For more accuracy, unfollow some and run again.');
+
+ this.display_output(html.join(''));
+ },
+
+ // Rounds to 10ths place.
+ r10: function(input) {
+ return Math.round(input * 10) / 10;
+ },
+
+ // Return how many of a needle string are in a haystack
+ count_occurrences: function(haystack, needle) {
+ count = 0;
+
+ for (var i = 0; i < haystack.length; i++) {
+ if (needle == haystack.substr(i,needle.length)) {
+ count++;
+ }
+ }
+
+ return count;
+ },
+
+ // Fill a user object with empty data
+ reset_user: function(user) {
+ for (var key in this.METRICS) {
+ user[key] = 0;
+ }
+ },
+
+ // User page
+ get_user: function(user, is_simulation) {
+ user = user.toLowerCase();
+ Unladen.simulated_users[user] = true;
+ Unladen.users[user] = false;
+
+ $.getJSON("/scan/user/?u=" + user, {}, function(json) {
+ var status = Unladen.process_tweets(json);
+ if (status !== true) {
+ if (is_simulation) {
+ Unladen.simulate_error(status);
+ } else {
+ Unladen.load_error(status);
+ }
+ } else {
+ Unladen.process_users();
+ }
+ $('#simulate_spinner').css('display', 'none');
+ });
+ },
+
+ load_error: function(status) {
+ var error_msg = '';
+ if (status == 401) {
+ error_msg = "You are not authenticated. <a href='/default/login/'>Please authorize with Twitter here</a>.";
+ } else if (status == -1) {
+ error_msg = "No tweets found. If you follow anybody, this means Twitter is having issues.";
+ } else {
+ error_msg = "We got an error trying to scan your tweets, likely because of Twitter being down. " + this.CONTACT_STRING + "<p>Twitter error #" + status + ".";
+ }
+ Unladen.display_output(error_msg);
+ },
+
+ simulate_error: function(status) {
+ var error_message = '';
+ if (status == 401) {
+ error_message = "User is private.";
+ } else if (status == -1) {
+ error_message = "No tweets.";
+ } else {
+ error_message = "Twitter error #" + status + ".";
+ }
+
+ $('#simulate_error').css('display', 'block'); // make show()
+ $('#simulate_error').html(error_message);
+ },
+
+ go_simulate: function() {
+ $('#simulate_spinner').css('display', 'inline');
+
+ this.get_user($('#simulate').val(), true);
+ return false; // Stop form from going
+ },
+
+ user_jump: function() {
+ // Jump from homepage to a user
+
+ window.location = "/u/" + $("#jumpto").val();
+
+ return false;
+ }
+
+};
View
963 public/javascripts/controls.js
@@ -0,0 +1,963 @@
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || { };
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {
+ setHeight: false,
+ offsetTop: element.offsetHeight
+ });
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--;
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++;
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.value;
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+};
+
+Ajax.InPlaceEditor = Class.create({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML.unescapeHTML();
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {