Well, it seems I forgot to add the acts_as_authenticated to the repository on my...
author<jlsharps@mit.edu> <>
Sat, 4 Aug 2007 04:43:54 +0000 (00:43 -0400)
committer<jlsharps@mit.edu> <>
Sat, 4 Aug 2007 04:43:54 +0000 (00:43 -0400)
71 files changed:
README
app/controllers/account_controller.rb [new file with mode: 0644]
app/controllers/application.rb
app/controllers/graphs_controller.rb [new file with mode: 0644]
app/controllers/quickvote_controller.rb
app/controllers/site_controller.rb
app/helpers/account_helper.rb [new file with mode: 0644]
app/models/account.rb [new file with mode: 0644]
app/views/account/index.rhtml [new file with mode: 0644]
app/views/account/login.rhtml [new file with mode: 0644]
app/views/account/signup.rhtml [new file with mode: 0644]
app/views/layouts/hc.rhtml
db/create.sql
db/migrate/004_create_users.rb [new file with mode: 0644]
lib/authenticated_system.rb [new file with mode: 0644]
lib/authenticated_test_helper.rb [new file with mode: 0644]
test/fixtures/users.yml [new file with mode: 0644]
test/functional/account_controller_test.rb [new file with mode: 0644]
test/functional/graphs_controller_test.rb [new file with mode: 0644]
test/unit/user_test.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/CHANGELOG [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/README [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/USAGE [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/authenticated_generator.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_system.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_test_helper.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/controller.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/fixtures.yml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/functional_test.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/helper.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/index.rhtml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/login.rhtml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/migration.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/model.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/signup.rhtml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated/templates/unit_test.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/USAGE [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/authenticated_mailer_generator.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/activation.rhtml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier_test.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/observer.rb [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/signup_notification.rhtml [new file with mode: 0644]
vendor/plugins/acts_as_authenticated/install.rb [new file with mode: 0644]
vendor/plugins/gruff/MIT-LICENSE [new file with mode: 0644]
vendor/plugins/gruff/README [new file with mode: 0644]
vendor/plugins/gruff/Rakefile [new file with mode: 0644]
vendor/plugins/gruff/about.yml [new file with mode: 0644]
vendor/plugins/gruff/generators/gruff/gruff_generator.rb [new file with mode: 0644]
vendor/plugins/gruff/generators/gruff/templates/controller.rb [new file with mode: 0644]
vendor/plugins/gruff/generators/gruff/templates/functional_test.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/accumulator_bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/area.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/bar_conversion.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/base.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/deprecated.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/line.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/mini/bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/mini/legend.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/mini/pie.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/mini/side_bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/net.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/photo_bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/pie.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/scene.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/side_bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/spider.rb [new file with mode: 0644]
vendor/plugins/gruff/lib/gruff/stacked_bar.rb [new file with mode: 0644]

diff --git a/README b/README
index 341ef6d10113abd1522342fc74d46924d7f3df0f..8c02056106234ea7a3d6abae90aa325caada93c8 100644 (file)
--- a/README
+++ b/README
@@ -22,4 +22,35 @@ http://technoweenie.stikipad.com/plugins/show/Acts+as+Authenticated is the
 best site for documentation regarding acts_as_authenticaed. Also, currently
 it only stores the user_id in the session, but i just found a guide to help 
 me make it store the entire user object, so I'll do that while my battery 
-charges.
\ No newline at end of file
+charges.
+
+08/03/07
+Handy trick: use the command 'gem_server' from a shell to create a server at 
+http://localhost:8008 that is an easy to navigate locally-hosted website with 
+all the documentation on local gems you have in a easy to read format.
+
+jlsharps: I added the Gruff plug-in today, which is viewable under the folder
+vender/plugins/gruff.  I installed it directly using the Gruff plug-in and
+included controller generate utility. The version 0.1.2, which doesn't seem to
+be the latest version. I've looked into it and it see and it seems that the
+latest version is 0.2.8. However, I wasn't sure how including a gem w/o a plugin 
+would function in end-game rails so I just what I used for now. If you guys 
+(mako of john) know how to do it, it'd probably be better to upgrade, but it 
+didn't seem like the best use of my time right now.  I got the plug-in here:
+http://topfunky.net/svn/plugins/gruff. You can get the gruff gem v 0.2.8 by 
+typing "sudo gem install gruff", I believe it's also hosted on RubyForge. 
+
+I created the GraphsController for Gruff methods to use. In Pollarize I put them 
+in the ApplicationContorller file, so they would be accessible to all. While 
+that it also an option here, it would also mean there wouldn't be much room for 
+playing around because everything in the Application file has to be perfect or 
+it seems to throw Error Code 500 (basically everything breaks). The show() 
+is a sample sample provided with Gruff. 
+
+Documentation is here:http://gruff.rubyforge.org/  Alternately, if you have the 
+gem installed, you can use the ri command, or the above mentioned gem_server. 
+
+If you guys want more helpful stuff here, let me know.
+
+
+
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
new file mode 100644 (file)
index 0000000..c13203b
--- /dev/null
@@ -0,0 +1,45 @@
+class AccountController < ApplicationController
+  layout 'hc'
+  
+  # Be sure to include AuthenticationSystem in Application Controller instead
+  include AuthenticatedSystem
+  # If you want "remember me" functionality, add this before_filter to Application Controller
+  before_filter :login_from_cookie
+
+  # say something nice, you goof!  something sweet.
+  def index
+    redirect_to(:action => 'signup') unless logged_in? || User.count > 0
+  end
+
+  def login
+    return unless request.post?
+    self.current_user = User.authenticate(params[:login], params[:password])
+    if logged_in?
+      if params[:remember_me] == "1"
+        self.current_user.remember_me
+        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
+      end
+      redirect_back_or_default(:controller => '/account', :action => 'index')
+      flash[:notice] = "Logged in successfully"
+    end
+  end
+
+  def signup
+    @user = User.new(params[:user])
+    return unless request.post?
+    @user.save!
+    self.current_user = @user
+    redirect_back_or_default(:controller => '/account', :action => 'index')
+    flash[:notice] = "Thanks for signing up!"
+  rescue ActiveRecord::RecordInvalid
+    render :action => 'signup'
+  end
+  
+  def logout
+    self.current_user.forget_me if logged_in?
+    cookies.delete :auth_token
+    reset_session
+    flash[:notice] = "You have been logged out."
+    redirect_back_or_default(:controller => '/account', :action => 'index')
+  end
+end
index d92ab9724e81d5b69043e75d84a0038377f78ab6..a5bb1085361dbdd7f9169c2d1a2d442384a15d3d 100644 (file)
@@ -2,6 +2,7 @@
 # Likewise, all the methods added will be available for all controllers.
 
 class ApplicationController < ActionController::Base
+  include AuthenticatedSystem
   helper :user
   model :user
 end
diff --git a/app/controllers/graphs_controller.rb b/app/controllers/graphs_controller.rb
new file mode 100644 (file)
index 0000000..c810853
--- /dev/null
@@ -0,0 +1,87 @@
+class GraphsController < ApplicationController
+
+  # To make caching easier, add a line like this to config/routes.rb:
+  # map.graph "graph/:action/:id/image.png", :controller => "graph"
+  #
+  # Then reference it with the named route:
+  #   image_tag graph_url(:action => 'show', :id => 42)
+
+  def show
+    g = Gruff::Line.new
+    # Uncomment to use your own theme or font
+    # See http://colourlovers.com or http://www.firewheeldesign.com/widgets/ for color ideas
+#     g.theme = {
+#       :colors => ['#663366', '#cccc99', '#cc6633', '#cc9966', '#99cc99'],
+#       :marker_color => 'white',
+#       :background_colors => ['black', '#333333']
+#     }
+#     g.font = File.expand_path('artwork/fonts/VeraBd.ttf', RAILS_ROOT)
+
+    g.title = "Gruff-o-Rama"
+    
+    g.data("Apples", [1, 2, 3, 4, 4, 3])
+    g.data("Oranges", [4, 8, 7, 9, 8, 9])
+    g.data("Watermelon", [2, 3, 1, 5, 6, 8])
+    g.data("Peaches", [9, 9, 10, 8, 7, 9])
+
+    g.labels = {0 => '2004', 2 => '2005', 4 => '2006'}
+
+    send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "gruff.png")
+  end
+  
+  #The following section has been pasted directly fromworking pollarize graphs
+  #and hasn't been adopted to fit Selectricity yet
+  
+  def day_votes
+    @poll = Poll.find(params[:id])
+    line = Gruff::Line.new
+    line.title = "Voters Per Day"
+    line.font = File.expand_path('/usr/X11R6/lib/X11/fonts/TTF/Vera.ttf', RAILS_ROOT)
+    line.data("#{@poll.name}", voter_days["voters_per_day"] )
+    line.labels = voter_days["days_hash"]
+    line.x_axis_label = "Date"
+    line.y_axis_label = "Number of Votes"
+    line.minimum_value = 0.0
+    line.draw
+    send_data(line.to_blob,
+              :disposition => 'inline',
+              :type => 'image/png',
+              :filename => "dayvotes#{@poll.id}.png")
+  end
+  
+  def voter_days
+    @poll = Poll.find(params[:id])
+    voter_times = Array.new
+    unique_days = Array.new
+    voters_per_day = Array.new
+    days_hash = Hash.new
+    
+    @poll.questions.each do |qstn|
+      qstn.votes.each do |vote|
+        voter_times << vote.time unless voter_times.any? {|utime| utime == vote.time}
+      end
+    end
+    voter_times.sort!
+    #find all times in voter_times with the same date, and then concatenate 
+    #that number onto votes_per_day
+    #
+    #doesn't work jsut yet
+    voter_times.each_with_index do |time, index| 
+      count = 1
+      unless unique_days.any? { |d1| d1.eql?(time.mon.to_s+"/"+time.day.to_s) }
+        unique_days << (time.mon.to_s+"/"+time.day.to_s)
+        count += (voter_times[(index+1)..-1].find_all {|t| t.mon == time.mon && t.day == time.day}).size
+        voters_per_day << count
+      end      
+    end
+    
+    unique_days.each_with_index do |fmtdate, index|
+      days_hash[index] = fmtdate
+    end    
+  return { "voters_per_day" => voters_per_day, "days_hash" => days_hash }
+   
+  end
+  #end copy/pasted section
+
+
+end
index 10815b3c590ac19af542240d3cf8ec6d420cb9e9..61a4c69261f714aba4461d7c1e5cf1b7a6c53e21 100644 (file)
@@ -10,6 +10,7 @@ class QuickvoteController < ApplicationController
   #############################################################
 
   def create
+    breakpoint
     if params[:quickvote] 
       @quickvote = QuickVote.new(params[:quickvote])
 
index 3307d88a27e0e31c0c9e9fa99ce1efafb6fdb595..dd049f7311d2bf0d95e03b7212bacc2b3565f12e 100644 (file)
@@ -10,7 +10,7 @@ class SiteController < ApplicationController
       # check to see that we actually have record of them
       if User.find_all(["id = ?", session[:user].id]).length == 1
         # if we have record of them, grab the list of their elections
-        session[:user] = User.find(session[:user].id)
+        session[:user] = User.find(session[:user])
         @current_elections = session[:user].elections.sort do |a,b|
           b.enddate <=> a.enddate
         end
diff --git a/app/helpers/account_helper.rb b/app/helpers/account_helper.rb
new file mode 100644 (file)
index 0000000..1b63056
--- /dev/null
@@ -0,0 +1,2 @@
+module AccountHelper
+end
\ No newline at end of file
diff --git a/app/models/account.rb b/app/models/account.rb
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/app/views/account/index.rhtml b/app/views/account/index.rhtml
new file mode 100644 (file)
index 0000000..d600d59
--- /dev/null
@@ -0,0 +1,56 @@
+<h1>In the Caboose.</h1>
+
+<% content_for 'poem' do -%>
+"Train delayed? and what's to say?" 
+"Blocked by last night's snow they say." 
+Seven hours or so to wait; 
+Well, that's pleasant! but there's the freight. 
+Depot loafing no one fancies, 
+We'll try the caboose and take our chances. 
+  
+Cool this morning in Watertown, 
+Somewhat frosty___mercury down; 
+Enter caboose___roaring fire, 
+With never an air-hole; heat so dire 
+That we shrivel and pant; we are roasted through- 
+Outside, thermometer thirty-two. 
+  
+We start with a jerk and suddenly stop. 
+"What's broke?" says one; another "What's up?", 
+"Oh, nothing," they answer, "That's our way: 
+You must stand the jerking, sorry to say." 
+We "stand it" with oft this painful thought: 
+Are our heads on yet, or are they not? 
+  
+Comrades in misery___let me see; 
+Girl like a statue opposite me; 
+Back and forth the others jostle___ 
+She never winks, nor moves a muscle; 
+See her, as she sits there now; 
+She's "well balanced," anyhow. 
+  
+Woman in trouble, tearful eyes, 
+Sits by the window, softly cries, 
+Pity___for griefs we may not know, 
+For breasts that ache, for tears that flow, 
+Though we know not why. Her eyelids red 
+Tell a sorrowful tale___some hope is dead. 
+  
+Man who follows the Golden Rule, 
+And lends his papers___a pocket full, 
+Has a blank book___once in a minute 
+Has an idea, and writes it in it. 
+Guess him? Yes, of course I can, 
+He's a___well___a newspaper man. 
+  
+Blue-eyed fairy, wrapped in fur; 
+Sweet young mother tending her. 
+Fairy thinks it's "awful far," 
+Wants to get off this "naughty car." 
+So do we, young golden-hair; 
+All this crowd are with you there!
+<% end -%>
+
+<%= simple_format @content_for_poem %>
+
+<p><a href="http://skyways.lib.ks.us/poetry/walls/caboose.html">-- Ellen P. Allerton.</a></p>
\ No newline at end of file
diff --git a/app/views/account/login.rhtml b/app/views/account/login.rhtml
new file mode 100644 (file)
index 0000000..a14ff99
--- /dev/null
@@ -0,0 +1,14 @@
+<% form_tag do -%>
+<p><label for="login">Login</label><br/>
+<%= text_field_tag 'login' %></p>
+
+<p><label for="password">Password</label><br/>
+<%= password_field_tag 'password' %></p>
+
+<!-- Uncomment this if you want this functionality
+<p><label for="remember_me">Remember me:</label>
+<%= check_box_tag 'remember_me' %></p>
+-->
+
+<p><%= submit_tag 'Log in' %></p>
+<% end -%>
diff --git a/app/views/account/signup.rhtml b/app/views/account/signup.rhtml
new file mode 100644 (file)
index 0000000..c0012a7
--- /dev/null
@@ -0,0 +1,16 @@
+<%= error_messages_for :user %>
+<% form_for :user do |f| -%>
+<p><label for="login">Login</label><br/>
+<%= f.text_field :login %></p>
+
+<p><label for="email">Email</label><br/>
+<%= f.text_field :email %></p>
+
+<p><label for="password">Password</label><br/>
+<%= f.password_field :password %></p>
+
+<p><label for="password_confirmation">Confirm Password</label><br/>
+<%= f.password_field :password_confirmation %></p>
+
+<p><%= submit_tag 'Sign up' %></p>
+<% end -%>
index 2408083e6bc14df624225056a8f92d1e3110987f..13b2e729109fc1d704d242418b840284a3f2d791 100755 (executable)
@@ -17,7 +17,7 @@
 
            <div id="links">
              <% if session[:user] %>
-<% breakpoint%>
+<% breakpoint %>
                Welcome <strong><%= User.find(session[:user]).login.capitalize %></strong>
              <% else %>
                <%= link_to("Login", :controller => "account", :action => "login")
index cee33a330ccf8cc2485066eee2fe9a12dbcb21f8..ff10db3a9357c4b5b57755ef3aa81801af49585e 100755 (executable)
@@ -68,6 +68,7 @@ create table votes (
  id int NOT NULL auto_increment,
  voter_id int DEFAULT NULL,
  confirmed tinyint NOT NULL DEFAULT 0,
+ time      datetime         DEFAULT NULL,
  constraint fk_vote_voter foreign key (voter_id) references voters(id),
  primary key (id)
 );
diff --git a/db/migrate/004_create_users.rb b/db/migrate/004_create_users.rb
new file mode 100644 (file)
index 0000000..9c78b5f
--- /dev/null
@@ -0,0 +1,18 @@
+class CreateUsers < ActiveRecord::Migration
+  def self.up
+    create_table "users", :force => true do |t|
+      t.column :login,                     :string
+      t.column :email,                     :string
+      t.column :crypted_password,          :string, :limit => 40
+      t.column :salt,                      :string, :limit => 40
+      t.column :created_at,                :datetime
+      t.column :updated_at,                :datetime
+      t.column :remember_token,            :string
+      t.column :remember_token_expires_at, :datetime
+    end
+  end
+
+  def self.down
+    drop_table "users"
+  end
+end
diff --git a/lib/authenticated_system.rb b/lib/authenticated_system.rb
new file mode 100644 (file)
index 0000000..840d89a
--- /dev/null
@@ -0,0 +1,120 @@
+module AuthenticatedSystem
+  protected
+    # Returns true or false if the user is logged in.
+    # Preloads @current_user with the user model if they're logged in.
+    def logged_in?
+      current_user != :false
+    end
+    
+    # Accesses the current user from the session.
+    def current_user
+      @current_user ||= (session[:user] && User.find_by_id(session[:user])) || :false
+    end
+    
+    # Store the given user in the session.
+    def current_user=(new_user)
+      session[:user] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
+      @current_user = new_user
+    end
+    
+    # Check if the user is authorized.
+    #
+    # Override this method in your controllers if you want to restrict access
+    # to only a few actions or if you want to check if the user
+    # has the correct rights.
+    #
+    # Example:
+    #
+    #  # only allow nonbobs
+    #  def authorize?
+    #    current_user.login != "bob"
+    #  end
+    def authorized?
+      true
+    end
+
+    # Filter method to enforce a login requirement.
+    #
+    # To require logins for all actions, use this in your controllers:
+    #
+    #   before_filter :login_required
+    #
+    # To require logins for specific actions, use this in your controllers:
+    #
+    #   before_filter :login_required, :only => [ :edit, :update ]
+    #
+    # To skip this in a subclassed controller:
+    #
+    #   skip_before_filter :login_required
+    #
+    def login_required
+      username, passwd = get_auth_data
+      self.current_user ||= User.authenticate(username, passwd) || :false if username && passwd
+      logged_in? && authorized? ? true : access_denied
+    end
+    
+    # Redirect as appropriate when an access request fails.
+    #
+    # The default action is to redirect to the login screen.
+    #
+    # Override this method in your controllers if you want to have special
+    # behavior in case the user is not authorized
+    # to access the requested action.  For example, a popup window might
+    # simply close itself.
+    def access_denied
+      respond_to do |accepts|
+        accepts.html do
+          store_location
+          redirect_to :controller => '/account', :action => 'login'
+        end
+        accepts.xml do
+          headers["Status"]           = "Unauthorized"
+          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
+          render :text => "Could't authenticate you", :status => '401 Unauthorized'
+        end
+      end
+      false
+    end  
+    
+    # Store the URI of the current request in the session.
+    #
+    # We can return to this location by calling #redirect_back_or_default.
+    def store_location
+      session[:return_to] = request.request_uri
+    end
+    
+    # Redirect to the URI stored by the most recent store_location call or
+    # to the passed default.
+    def redirect_back_or_default(default)
+      session[:return_to] ? redirect_to_url(session[:return_to]) : redirect_to(default)
+      session[:return_to] = nil
+    end
+    
+    # Inclusion hook to make #current_user and #logged_in?
+    # available as ActionView helper methods.
+    def self.included(base)
+      base.send :helper_method, :current_user, :logged_in?
+    end
+
+    # When called with before_filter :login_from_cookie will check for an :auth_token
+    # cookie and log the user back in if apropriate
+    def login_from_cookie
+      return unless cookies[:auth_token] && !logged_in?
+      user = User.find_by_remember_token(cookies[:auth_token])
+      if user && user.remember_token?
+        user.remember_me
+        self.current_user = user
+        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
+        flash[:notice] = "Logged in successfully"
+      end
+    end
+
+  private
+    @@http_auth_headers = %w(X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION Authorization)
+    # gets BASIC auth info
+    def get_auth_data
+      auth_key  = @@http_auth_headers.detect { |h| request.env.has_key?(h) }
+      auth_data = request.env[auth_key].to_s.split unless auth_key.blank?
+      return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil] 
+    end
+end
diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb
new file mode 100644 (file)
index 0000000..a704035
--- /dev/null
@@ -0,0 +1,113 @@
+module AuthenticatedTestHelper
+  # Sets the current user in the session from the user fixtures.
+  def login_as(user)
+    @request.session[:user] = user ? users(user).id : nil
+  end
+
+  def content_type(type)
+    @request.env['Content-Type'] = type
+  end
+
+  def accept(accept)
+    @request.env["HTTP_ACCEPT"] = accept
+  end
+
+  def authorize_as(user)
+    if user
+      @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}"
+      accept       'application/xml'
+      content_type 'application/xml'
+    else
+      @request.env["HTTP_AUTHORIZATION"] = nil
+      accept       nil
+      content_type nil
+    end
+  end
+
+  # http://project.ioni.st/post/217#post-217
+  #
+  #  def test_new_publication
+  #    assert_difference(Publication, :count) do
+  #      post :create, :publication => {...}
+  #      # ...
+  #    end
+  #  end
+  # 
+  def assert_difference(object, method = nil, difference = 1)
+    initial_value = object.send(method)
+    yield
+    assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
+  end
+
+  def assert_no_difference(object, method, &block)
+    assert_difference object, method, 0, &block
+  end
+
+  # Assert the block redirects to the login
+  # 
+  #   assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
+  #
+  def assert_requires_login(login = nil)
+    yield HttpLoginProxy.new(self, login)
+  end
+
+  def assert_http_authentication_required(login = nil)
+    yield XmlLoginProxy.new(self, login)
+  end
+
+  def reset!(*instance_vars)
+    instance_vars = [:controller, :request, :response] unless instance_vars.any?
+    instance_vars.collect! { |v| "@#{v}".to_sym }
+    instance_vars.each do |var|
+      instance_variable_set(var, instance_variable_get(var).class.new)
+    end
+  end
+end
+
+class BaseLoginProxy
+  attr_reader :controller
+  attr_reader :options
+  def initialize(controller, login)
+    @controller = controller
+    @login      = login
+  end
+
+  private
+    def authenticated
+      raise NotImplementedError
+    end
+    
+    def check
+      raise NotImplementedError
+    end
+    
+    def method_missing(method, *args)
+      @controller.reset!
+      authenticate
+      @controller.send(method, *args)
+      check
+    end
+end
+
+class HttpLoginProxy < BaseLoginProxy
+  protected
+    def authenticate
+      @controller.login_as @login if @login
+    end
+    
+    def check
+      @controller.assert_redirected_to :controller => 'account', :action => 'login'
+    end
+end
+
+class XmlLoginProxy < BaseLoginProxy
+  protected
+    def authenticate
+      @controller.accept 'application/xml'
+      @controller.authorize_as @login if @login
+    end
+    
+    def check
+      @controller.assert_response 401
+    end
+end
\ No newline at end of file
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
new file mode 100644 (file)
index 0000000..f7be9db
--- /dev/null
@@ -0,0 +1,17 @@
+quentin:
+  id: 1
+  login: quentin
+  email: quentin@example.com
+  salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
+  crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
+  #crypted_password: "ce2/iFrNtQ8=\n" # quentin, use only if you're using 2-way encryption
+  created_at: <%= 5.days.ago.to_s :db %>
+  # activated_at: <%= 5.days.ago.to_s :db %> # only if you're activating new signups
+aaron:
+  id: 2
+  login: aaron
+  email: aaron@example.com
+  salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
+  crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
+  # activation_code: aaronscode # only if you're activating new signups
+  created_at: <%= 1.days.ago.to_s :db %>
\ No newline at end of file
diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb
new file mode 100644 (file)
index 0000000..3c8cd22
--- /dev/null
@@ -0,0 +1,129 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'account_controller'
+
+# Re-raise errors caught by the controller.
+class AccountController; def rescue_action(e) raise e end; end
+
+class AccountControllerTest < Test::Unit::TestCase
+  # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead
+  # Then, you can remove it from this and the units test.
+  include AuthenticatedTestHelper
+
+  fixtures :users
+
+  def setup
+    @controller = AccountController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  def test_should_login_and_redirect
+    post :login, :login => 'quentin', :password => 'test'
+    assert session[:user]
+    assert_response :redirect
+  end
+
+  def test_should_fail_login_and_not_redirect
+    post :login, :login => 'quentin', :password => 'bad password'
+    assert_nil session[:user]
+    assert_response :success
+  end
+
+  def test_should_allow_signup
+    assert_difference User, :count do
+      create_user
+      assert_response :redirect
+    end
+  end
+
+  def test_should_require_login_on_signup
+    assert_no_difference User, :count do
+      create_user(:login => nil)
+      assert assigns(:user).errors.on(:login)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_password_on_signup
+    assert_no_difference User, :count do
+      create_user(:password => nil)
+      assert assigns(:user).errors.on(:password)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_password_confirmation_on_signup
+    assert_no_difference User, :count do
+      create_user(:password_confirmation => nil)
+      assert assigns(:user).errors.on(:password_confirmation)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_email_on_signup
+    assert_no_difference User, :count do
+      create_user(:email => nil)
+      assert assigns(:user).errors.on(:email)
+      assert_response :success
+    end
+  end
+
+  def test_should_logout
+    login_as :quentin
+    get :logout
+    assert_nil session[:user]
+    assert_response :redirect
+  end
+
+  def test_should_remember_me
+    post :login, :login => 'quentin', :password => 'test', :remember_me => "1"
+    assert_not_nil @response.cookies["auth_token"]
+  end
+
+  def test_should_not_remember_me
+    post :login, :login => 'quentin', :password => 'test', :remember_me => "0"
+    assert_nil @response.cookies["auth_token"]
+  end
+  
+  def test_should_delete_token_on_logout
+    login_as :quentin
+    get :logout
+    assert_equal @response.cookies["auth_token"], []
+  end
+
+  def test_should_login_with_cookie
+    users(:quentin).remember_me
+    @request.cookies["auth_token"] = cookie_for(:quentin)
+    get :index
+    assert @controller.send(:logged_in?)
+  end
+
+  def test_should_fail_expired_cookie_login
+    users(:quentin).remember_me
+    users(:quentin).update_attribute :remember_token_expires_at, 5.minutes.ago
+    @request.cookies["auth_token"] = cookie_for(:quentin)
+    get :index
+    assert !@controller.send(:logged_in?)
+  end
+
+  def test_should_fail_cookie_login
+    users(:quentin).remember_me
+    @request.cookies["auth_token"] = auth_token('invalid_auth_token')
+    get :index
+    assert !@controller.send(:logged_in?)
+  end
+
+  protected
+    def create_user(options = {})
+      post :signup, :user => { :login => 'quire', :email => 'quire@example.com', 
+        :password => 'quire', :password_confirmation => 'quire' }.merge(options)
+    end
+    
+    def auth_token(token)
+      CGI::Cookie.new('name' => 'auth_token', 'value' => token)
+    end
+    
+    def cookie_for(user)
+      auth_token users(user).remember_token
+    end
+end
diff --git a/test/functional/graphs_controller_test.rb b/test/functional/graphs_controller_test.rb
new file mode 100644 (file)
index 0000000..8e206d3
--- /dev/null
@@ -0,0 +1,24 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require 'graphs_controller'
+
+# Re-raise errors caught by the controller.
+class GraphsController; def rescue_action(e) raise e end; end
+
+class GraphsControllerTest < Test::Unit::TestCase
+
+  #fixtures :data
+
+  def setup
+    @controller = GraphsController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  # TODO Replace this with your actual tests
+  def test_show
+    get :show
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+  
+end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
new file mode 100644 (file)
index 0000000..8166c09
--- /dev/null
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UserTest < Test::Unit::TestCase
+  # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead.
+  # Then, you can remove it from this and the functional test.
+  include AuthenticatedTestHelper
+  fixtures :users
+
+  def test_should_create_user
+    assert_difference User, :count do
+      user = create_user
+      assert !user.new_record?, "#{user.errors.full_messages.to_sentence}"
+    end
+  end
+
+  def test_should_require_login
+    assert_no_difference User, :count do
+      u = create_user(:login => nil)
+      assert u.errors.on(:login)
+    end
+  end
+
+  def test_should_require_password
+    assert_no_difference User, :count do
+      u = create_user(:password => nil)
+      assert u.errors.on(:password)
+    end
+  end
+
+  def test_should_require_password_confirmation
+    assert_no_difference User, :count do
+      u = create_user(:password_confirmation => nil)
+      assert u.errors.on(:password_confirmation)
+    end
+  end
+
+  def test_should_require_email
+    assert_no_difference User, :count do
+      u = create_user(:email => nil)
+      assert u.errors.on(:email)
+    end
+  end
+
+  def test_should_reset_password
+    users(:quentin).update_attributes(:password => 'new password', :password_confirmation => 'new password')
+    assert_equal users(:quentin), User.authenticate('quentin', 'new password')
+  end
+
+  def test_should_not_rehash_password
+    users(:quentin).update_attributes(:login => 'quentin2')
+    assert_equal users(:quentin), User.authenticate('quentin2', 'test')
+  end
+
+  def test_should_authenticate_user
+    assert_equal users(:quentin), User.authenticate('quentin', 'test')
+  end
+
+  def test_should_set_remember_token
+    users(:quentin).remember_me
+    assert_not_nil users(:quentin).remember_token
+    assert_not_nil users(:quentin).remember_token_expires_at
+  end
+
+  def test_should_unset_remember_token
+    users(:quentin).remember_me
+    assert_not_nil users(:quentin).remember_token
+    users(:quentin).forget_me
+    assert_nil users(:quentin).remember_token
+  end
+
+  protected
+    def create_user(options = {})
+      User.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options))
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/CHANGELOG b/vendor/plugins/acts_as_authenticated/CHANGELOG
new file mode 100644 (file)
index 0000000..339de30
--- /dev/null
@@ -0,0 +1,86 @@
+* (1 Aug 2006)
+
+  Add error_messages_for to signup.rhtml
+  Favor save! over save in #signup
+  Refactor Authenticated System, actually use access_denied again
+  Change arthur fixture to aaron stack, the machine man!
+
+* (3 July 2006)
+
+  Add cookie token support [cnf]
+
+* (12 June 2006)
+
+  Fixed logged_in? so it does not keep hitting the database on every call if the user is not logged in.
+  Removed useless protect? method
+  Removed required 'user' argument of authorized?.  Access action_name and current_user to determine authorization.
+
+* (28 March 2006)
+
+  Removed "extra credit" documentation into wiki: http://technoweenie.stikipad.com/plugins/show/Acts+as+Authenticated
+  Stop validating the user's salt since they can't even fix that if they wanted to.
+
+* (25 March 2006)
+
+  Added account_location plugin hooks
+
+* (20 March 2006)
+
+  New for Rails 1.1!
+
+  Added #assert_difference and #assert_no_difference test helpers
+  Changed signup form to use #form_for
+
+* (22 Jan 2006)
+
+  Fixed user validations
+  Fixed Migration table name
+
+* (19 Jan 2006)
+
+  Fixed generator erb bug [Kyle Maxwell]
+
+* (18 Jan 2006)
+
+  Fixed bug with password field adding validation errors [Chris Nolan]
+  Added persistent login example [Chris Nolan]
+
+* (17 Dec 2005)
+
+  Various typos fixed.  Examples for using an activation code added [Ben Bleything]
+
+* (6 Dec 2005)
+
+  Add some docs to README, add install.rb script
+
+* (20 Nov 2005)
+
+  Redirect to signup page if no users have been created [Bousquet]
+
+  Experimental generator for creating the users table.  Requires edge rails and does not let you set the table name:
+  
+    ./script/generate authenticated_migration
+
+* (18 Nov 2005)
+
+  Allow changing of the login model name when generating the observer [Bill Katz]
+
+* (15 Nov 2005)
+
+  Commented out the activate action in the controller [tobyjoe]
+  fixed notifier.rb's erb variables [tobyjoe]
+  removed last old instance of set_current_user [tobyjoe]
+  
+
+* (13 Nov 2005)
+
+  fixed assert_login_required bug with :controller => '/account'
+  tweak logged_in? method to check for valid current_user, not just any old id value
+  comment out user activation by default
+  added index template, updated login_required comments [corp]
+  removed troublesome login_required class method.  use the filter instead
+
+* (12 Nov 2005) 
+
+  renaming generators to authentication and authentication_mailer
+  store user ID in session by default
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/README b/vendor/plugins/acts_as_authenticated/README
new file mode 100644 (file)
index 0000000..13c900e
--- /dev/null
@@ -0,0 +1,20 @@
+acts_as_authenticated generator
+====
+
+This is a basic authentication generator for rails, very much in the spirit of xal's original Login Generator.
+
+To use:
+
+  ./script/generate authenticated user account
+
+This generates a basic user model, a controller, some basic views, and tests.  Extra functionality can be unlocked by
+removing the comments for them.  I have a few examples such as user activation and reversible encrypted passwords.
+
+The user migration is also generated unless you pass --skip-migration.
+
+Generate your mailer:
+
+  ./script/generate authenticated_mailer user
+
+Consult the Acts As Authenticated wiki for more: http://technoweenie.stikipad.com/plugins/show/Acts+as+Authenticated
+   
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/USAGE b/vendor/plugins/acts_as_authenticated/generators/authenticated/USAGE
new file mode 100644 (file)
index 0000000..72794d7
--- /dev/null
@@ -0,0 +1 @@
+./script/generate authenticated USERMODEL CONTROLLERNAME
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/authenticated_generator.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/authenticated_generator.rb
new file mode 100644 (file)
index 0000000..9c8a625
--- /dev/null
@@ -0,0 +1,102 @@
+class AuthenticatedGenerator < Rails::Generator::NamedBase
+  attr_reader   :controller_name,
+                :controller_class_path,
+                :controller_file_path,
+                :controller_class_nesting,
+                :controller_class_nesting_depth,
+                :controller_class_name,
+                :controller_singular_name,
+                :controller_plural_name
+  alias_method  :controller_file_name,  :controller_singular_name
+  alias_method  :controller_table_name, :controller_plural_name
+
+  def initialize(runtime_args, runtime_options = {})
+    super
+
+    # Take controller name from the next argument.  Default to the pluralized model name.
+    @controller_name = args.shift
+    @controller_name ||= ActiveRecord::Base.pluralize_table_names ? @name.pluralize : @name
+
+    base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
+    @controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name)
+
+    if @controller_class_nesting.empty?
+      @controller_class_name = @controller_class_name_without_nesting
+    else
+      @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
+    end
+  end
+
+  def manifest
+    record do |m|
+      # Check for class naming collisions.
+      m.class_collisions controller_class_path, "#{controller_class_name}Controller", 
+                                                #"#{controller_class_name}ControllerTest", 
+                                                "#{controller_class_name}Helper"
+      m.class_collisions class_path,            "#{class_name}"
+                                                #"#{class_name}Test"
+      m.class_collisions [], 'AuthenticatedSystem', 'AuthenticatedTestHelper'
+
+      # Controller, helper, views, and test directories.
+      m.directory File.join('app/models', class_path)
+      m.directory File.join('app/controllers', controller_class_path)
+      m.directory File.join('app/helpers', controller_class_path)
+      m.directory File.join('app/views', controller_class_path, controller_file_name)
+      m.directory File.join('test/functional', controller_class_path)
+      m.directory File.join('test/unit', class_path)
+
+      m.template 'model.rb',
+                  File.join('app/models',
+                            class_path,
+                            "#{file_name}.rb")
+
+      m.template 'controller.rb',
+                  File.join('app/controllers',
+                            controller_class_path,
+                            "#{controller_file_name}_controller.rb")
+
+      m.template 'authenticated_system.rb',
+                  File.join('lib', 'authenticated_system.rb')
+
+      m.template 'authenticated_test_helper.rb',
+                  File.join('lib', 'authenticated_test_helper.rb')
+
+      m.template 'functional_test.rb',
+                  File.join('test/functional',
+                            controller_class_path,
+                            "#{controller_file_name}_controller_test.rb")
+
+      m.template 'helper.rb',
+                  File.join('app/helpers',
+                            controller_class_path,
+                            "#{controller_file_name}_helper.rb")
+
+      m.template 'unit_test.rb',
+                  File.join('test/unit',
+                            class_path,
+                            "#{file_name}_test.rb")
+
+      m.template 'fixtures.yml',
+                  File.join('test/fixtures',
+                            "#{table_name}.yml")
+
+      # Controller templates
+      %w( index login signup ).each do |action|
+        m.template "#{action}.rhtml",
+                   File.join('app/views', controller_class_path, controller_file_name, "#{action}.rhtml")
+      end
+
+      unless options[:skip_migration]
+        m.migration_template 'migration.rb', 'db/migrate', :assigns => {
+          :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
+        }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
+      end
+    end
+  end
+
+  protected
+    # Override with your own usage banner.
+    def banner
+      "Usage: #{$0} authenticated ModelName [ControllerName]"
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_system.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_system.rb
new file mode 100644 (file)
index 0000000..4939ba3
--- /dev/null
@@ -0,0 +1,121 @@
+module AuthenticatedSystem
+  protected
+    # Returns true or false if the user is logged in.
+    # Preloads @current_<%= file_name %> with the user model if they're logged in.
+      
+    def logged_in?
+    current_<%= file_name %> != :false 
+    end
+
+    # Accesses the current <%= file_name %> from the session.
+    def current_<%= file_name %>      
+    @current_<%= file_name %> ||= (session[:<%= file_name %>] && <%= class_name %>.find_by_id(session[:<%= file_name %>])) || :false
+    end
+
+     # Store the given <%= file_name %> in the session.
+    def current_<%= file_name %>=(new_<%= file_name %>)
+      session[:<%= file_name %>] = (new_<%= file_name %>.nil? || new_<%= file_name %>.is_a?(Symbol)) ? nil : new_<%= file_name %>.id
+      @current_<%= file_name %> = new_<%= file_name %>
+    end
+    
+    # Check if the <%= file_name %> is authorized.
+    #
+    # Override this method in your controllers if you want to restrict access
+    # to only a few actions or if you want to check if the <%= file_name %>
+    # has the correct rights.
+    #
+    # Example:
+    #
+    #  # only allow nonbobs
+    #  def authorize?
+    #    current_<%= file_name %>.login != "bob"
+    #  end
+    def authorized?
+      true
+    end
+
+    # Filter method to enforce a login requirement.
+    #
+    # To require logins for all actions, use this in your controllers:
+    #
+    #   before_filter :login_required
+    #
+    # To require logins for specific actions, use this in your controllers:
+    #
+    #   before_filter :login_required, :only => [ :edit, :update ]
+    #
+    # To skip this in a subclassed controller:
+    #
+    #   skip_before_filter :login_required
+    #
+    def login_required
+      username, passwd = get_auth_data
+      self.current_<%= file_name %> ||= <%= class_name %>.authenticate(username, passwd) || :false if username && passwd
+      logged_in? && authorized? ? true : access_denied
+    end
+    
+    # Redirect as appropriate when an access request fails.
+    #
+    # The default action is to redirect to the login screen.
+    #
+    # Override this method in your controllers if you want to have special
+    # behavior in case the <%= file_name %> is not authorized
+    # to access the requested action.  For example, a popup window might
+    # simply close itself.
+    def access_denied
+      respond_to do |accepts|
+        accepts.html do
+          store_location
+          redirect_to :controller => '/<%= controller_file_name %>', :action => 'login'
+        end
+        accepts.xml do
+          headers["Status"]           = "Unauthorized"
+          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
+          render :text => "Could't authenticate you", :status => '401 Unauthorized'
+        end
+      end
+      false
+    end  
+    
+    # Store the URI of the current request in the session.
+    #
+    # We can return to this location by calling #redirect_back_or_default.
+    def store_location
+      session[:return_to] = request.request_uri
+    end
+    
+    # Redirect to the URI stored by the most recent store_location call or
+    # to the passed default.
+    def redirect_back_or_default(default)
+      session[:return_to] ? redirect_to_url(session[:return_to]) : redirect_to(default)
+      session[:return_to] = nil
+    end
+    
+    # Inclusion hook to make #current_<%= file_name %> and #logged_in?
+    # available as ActionView helper methods.
+    def self.included(base)
+      base.send :helper_method, :current_<%= file_name %>, :logged_in?
+    end
+
+    # When called with before_filter :login_from_cookie will check for an :auth_token
+    # cookie and log the user back in if apropriate
+    def login_from_cookie
+      return unless cookies[:auth_token] && !logged_in?
+      user = <%= class_name %>.find_by_remember_token(cookies[:auth_token])
+      if user && user.remember_token?
+        user.remember_me
+        self.current_<%= file_name %> = user
+        cookies[:auth_token] = { :value => self.current_<%= file_name %>.remember_token , :expires => self.current_<%= file_name %>.remember_token_expires_at }
+        flash[:notice] = "Logged in successfully"
+      end
+    end
+
+  private
+    @@http_auth_headers = %w(X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION Authorization)
+    # gets BASIC auth info
+    def get_auth_data
+      auth_key  = @@http_auth_headers.detect { |h| request.env.has_key?(h) }
+      auth_data = request.env[auth_key].to_s.split unless auth_key.blank?
+      return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil] 
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_test_helper.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/authenticated_test_helper.rb
new file mode 100644 (file)
index 0000000..0f2cd03
--- /dev/null
@@ -0,0 +1,113 @@
+module AuthenticatedTestHelper
+  # Sets the current <%= file_name %> in the session from the <%= file_name %> fixtures.
+  def login_as(<%= file_name %>)
+    @request.session[:<%= file_name %>] = <%= file_name %> ? <%= table_name %>(<%= file_name %>).id : nil
+  end
+
+  def content_type(type)
+    @request.env['Content-Type'] = type
+  end
+
+  def accept(accept)
+    @request.env["HTTP_ACCEPT"] = accept
+  end
+
+  def authorize_as(user)
+    if user
+      @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}"
+      accept       'application/xml'
+      content_type 'application/xml'
+    else
+      @request.env["HTTP_AUTHORIZATION"] = nil
+      accept       nil
+      content_type nil
+    end
+  end
+
+  # http://project.ioni.st/post/217#post-217
+  #
+  #  def test_new_publication
+  #    assert_difference(Publication, :count) do
+  #      post :create, :publication => {...}
+  #      # ...
+  #    end
+  #  end
+  # 
+  def assert_difference(object, method = nil, difference = 1)
+    initial_value = object.send(method)
+    yield
+    assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
+  end
+
+  def assert_no_difference(object, method, &block)
+    assert_difference object, method, 0, &block
+  end
+
+  # Assert the block redirects to the login
+  # 
+  #   assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
+  #
+  def assert_requires_login(login = nil)
+    yield HttpLoginProxy.new(self, login)
+  end
+
+  def assert_http_authentication_required(login = nil)
+    yield XmlLoginProxy.new(self, login)
+  end
+
+  def reset!(*instance_vars)
+    instance_vars = [:controller, :request, :response] unless instance_vars.any?
+    instance_vars.collect! { |v| "@#{v}".to_sym }
+    instance_vars.each do |var|
+      instance_variable_set(var, instance_variable_get(var).class.new)
+    end
+  end
+end
+
+class BaseLoginProxy
+  attr_reader :controller
+  attr_reader :options
+  def initialize(controller, login)
+    @controller = controller
+    @login      = login
+  end
+
+  private
+    def authenticated
+      raise NotImplementedError
+    end
+    
+    def check
+      raise NotImplementedError
+    end
+    
+    def method_missing(method, *args)
+      @controller.reset!
+      authenticate
+      @controller.send(method, *args)
+      check
+    end
+end
+
+class HttpLoginProxy < BaseLoginProxy
+  protected
+    def authenticate
+      @controller.login_as @login if @login
+    end
+    
+    def check
+      @controller.assert_redirected_to :controller => 'account', :action => 'login'
+    end
+end
+
+class XmlLoginProxy < BaseLoginProxy
+  protected
+    def authenticate
+      @controller.accept 'application/xml'
+      @controller.authorize_as @login if @login
+    end
+    
+    def check
+      @controller.assert_response 401
+    end
+end
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/controller.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/controller.rb
new file mode 100644 (file)
index 0000000..56a460b
--- /dev/null
@@ -0,0 +1,43 @@
+class <%= controller_class_name %>Controller < ApplicationController
+  # Be sure to include AuthenticationSystem in Application Controller instead
+  include AuthenticatedSystem
+  # If you want "remember me" functionality, add this before_filter to Application Controller
+  before_filter :login_from_cookie
+
+  # say something nice, you goof!  something sweet.
+  def index
+    redirect_to(:action => 'signup') unless logged_in? || <%= class_name %>.count > 0
+  end
+
+  def login
+    return unless request.post?
+    self.current_<%= file_name %> = <%= class_name %>.authenticate(params[:login], params[:password])
+    if logged_in?
+      if params[:remember_me] == "1"
+        self.current_<%= file_name %>.remember_me
+        cookies[:auth_token] = { :value => self.current_<%= file_name %>.remember_token , :expires => self.current_<%= file_name %>.remember_token_expires_at }
+      end
+      redirect_back_or_default(:controller => '/<%= controller_file_name %>', :action => 'index')
+      flash[:notice] = "Logged in successfully"
+    end
+  end
+
+  def signup
+    @<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>])
+    return unless request.post?
+    @<%= file_name %>.save!
+    self.current_<%= file_name %> = @<%= file_name %>
+    redirect_back_or_default(:controller => '/<%= controller_file_name %>', :action => 'index')
+    flash[:notice] = "Thanks for signing up!"
+  rescue ActiveRecord::RecordInvalid
+    render :action => 'signup'
+  end
+  
+  def logout
+    self.current_<%= file_name %>.forget_me if logged_in?
+    cookies.delete :auth_token
+    reset_session
+    flash[:notice] = "You have been logged out."
+    redirect_back_or_default(:controller => '/<%= controller_file_name %>', :action => 'index')
+  end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/fixtures.yml b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/fixtures.yml
new file mode 100644 (file)
index 0000000..f410fe9
--- /dev/null
@@ -0,0 +1,17 @@
+quentin:
+  id: 1
+  login: quentin
+  email: quentin@example.com
+  salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
+  crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
+  #crypted_password: "ce2/iFrNtQ8=\n" # quentin, use only if you're using 2-way encryption
+  created_at: <%%= 5.days.ago.to_s :db %>
+  # activated_at: <%%= 5.days.ago.to_s :db %> # only if you're activating new signups
+aaron:
+  id: 2
+  login: aaron
+  email: aaron@example.com
+  salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
+  crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1 # test
+  # activation_code: aaronscode # only if you're activating new signups
+  created_at: <%%= 1.days.ago.to_s :db %>
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/functional_test.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/functional_test.rb
new file mode 100644 (file)
index 0000000..48273e8
--- /dev/null
@@ -0,0 +1,129 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= controller_file_name %>_controller'
+
+# Re-raise errors caught by the controller.
+class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end
+
+class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase
+  # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead
+  # Then, you can remove it from this and the units test.
+  include AuthenticatedTestHelper
+
+  fixtures :<%= table_name %>
+
+  def setup
+    @controller = <%= controller_class_name %>Controller.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  def test_should_login_and_redirect
+    post :login, :login => 'quentin', :password => 'test'
+    assert session[:<%= file_name %>]
+    assert_response :redirect
+  end
+
+  def test_should_fail_login_and_not_redirect
+    post :login, :login => 'quentin', :password => 'bad password'
+    assert_nil session[:<%= file_name %>]
+    assert_response :success
+  end
+
+  def test_should_allow_signup
+    assert_difference <%= class_name %>, :count do
+      create_<%= file_name %>
+      assert_response :redirect
+    end
+  end
+
+  def test_should_require_login_on_signup
+    assert_no_difference <%= class_name %>, :count do
+      create_<%= file_name %>(:login => nil)
+      assert assigns(:<%= file_name %>).errors.on(:login)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_password_on_signup
+    assert_no_difference <%= class_name %>, :count do
+      create_<%= file_name %>(:password => nil)
+      assert assigns(:<%= file_name %>).errors.on(:password)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_password_confirmation_on_signup
+    assert_no_difference <%= class_name %>, :count do
+      create_<%= file_name %>(:password_confirmation => nil)
+      assert assigns(:<%= file_name %>).errors.on(:password_confirmation)
+      assert_response :success
+    end
+  end
+
+  def test_should_require_email_on_signup
+    assert_no_difference <%= class_name %>, :count do
+      create_<%= file_name %>(:email => nil)
+      assert assigns(:<%= file_name %>).errors.on(:email)
+      assert_response :success
+    end
+  end
+
+  def test_should_logout
+    login_as :quentin
+    get :logout
+    assert_nil session[:<%= file_name %>]
+    assert_response :redirect
+  end
+
+  def test_should_remember_me
+    post :login, :login => 'quentin', :password => 'test', :remember_me => "1"
+    assert_not_nil @response.cookies["auth_token"]
+  end
+
+  def test_should_not_remember_me
+    post :login, :login => 'quentin', :password => 'test', :remember_me => "0"
+    assert_nil @response.cookies["auth_token"]
+  end
+  
+  def test_should_delete_token_on_logout
+    login_as :quentin
+    get :logout
+    assert_equal @response.cookies["auth_token"], []
+  end
+
+  def test_should_login_with_cookie
+    <%= table_name %>(:quentin).remember_me
+    @request.cookies["auth_token"] = cookie_for(:quentin)
+    get :index
+    assert @controller.send(:logged_in?)
+  end
+
+  def test_should_fail_expired_cookie_login
+    <%= table_name %>(:quentin).remember_me
+    users(:quentin).update_attribute :remember_token_expires_at, 5.minutes.ago
+    @request.cookies["auth_token"] = cookie_for(:quentin)
+    get :index
+    assert !@controller.send(:logged_in?)
+  end
+
+  def test_should_fail_cookie_login
+    <%= table_name %>(:quentin).remember_me
+    @request.cookies["auth_token"] = auth_token('invalid_auth_token')
+    get :index
+    assert !@controller.send(:logged_in?)
+  end
+
+  protected
+    def create_<%= file_name %>(options = {})
+      post :signup, :<%= file_name %> => { :login => 'quire', :email => 'quire@example.com', 
+        :password => 'quire', :password_confirmation => 'quire' }.merge(options)
+    end
+    
+    def auth_token(token)
+      CGI::Cookie.new('name' => 'auth_token', 'value' => token)
+    end
+    
+    def cookie_for(<%= file_name %>)
+      auth_token <%= table_name %>(<%= file_name %>).remember_token
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/helper.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/helper.rb
new file mode 100644 (file)
index 0000000..ed5759e
--- /dev/null
@@ -0,0 +1,2 @@
+module <%= controller_class_name %>Helper
+end
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/index.rhtml b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/index.rhtml
new file mode 100644 (file)
index 0000000..9c706f8
--- /dev/null
@@ -0,0 +1,56 @@
+<h1>In the Caboose.</h1>
+
+<%% content_for 'poem' do -%>
+"Train delayed? and what's to say?" 
+"Blocked by last night's snow they say." 
+Seven hours or so to wait; 
+Well, that's pleasant! but there's the freight. 
+Depot loafing no one fancies, 
+We'll try the caboose and take our chances. 
+  
+Cool this morning in Watertown, 
+Somewhat frosty___mercury down; 
+Enter caboose___roaring fire, 
+With never an air-hole; heat so dire 
+That we shrivel and pant; we are roasted through- 
+Outside, thermometer thirty-two. 
+  
+We start with a jerk and suddenly stop. 
+"What's broke?" says one; another "What's up?", 
+"Oh, nothing," they answer, "That's our way: 
+You must stand the jerking, sorry to say." 
+We "stand it" with oft this painful thought: 
+Are our heads on yet, or are they not? 
+  
+Comrades in misery___let me see; 
+Girl like a statue opposite me; 
+Back and forth the others jostle___ 
+She never winks, nor moves a muscle; 
+See her, as she sits there now; 
+She's "well balanced," anyhow. 
+  
+Woman in trouble, tearful eyes, 
+Sits by the window, softly cries, 
+Pity___for griefs we may not know, 
+For breasts that ache, for tears that flow, 
+Though we know not why. Her eyelids red 
+Tell a sorrowful tale___some hope is dead. 
+  
+Man who follows the Golden Rule, 
+And lends his papers___a pocket full, 
+Has a blank book___once in a minute 
+Has an idea, and writes it in it. 
+Guess him? Yes, of course I can, 
+He's a___well___a newspaper man. 
+  
+Blue-eyed fairy, wrapped in fur; 
+Sweet young mother tending her. 
+Fairy thinks it's "awful far," 
+Wants to get off this "naughty car." 
+So do we, young golden-hair; 
+All this crowd are with you there!
+<%% end -%>
+
+<%%= simple_format @content_for_poem %>
+
+<p><a href="http://skyways.lib.ks.us/poetry/walls/caboose.html">-- Ellen P. Allerton.</a></p>
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/login.rhtml b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/login.rhtml
new file mode 100644 (file)
index 0000000..98c4037
--- /dev/null
@@ -0,0 +1,14 @@
+<%% form_tag do -%>
+<p><label for="login">Login</label><br/>
+<%%= text_field_tag 'login' %></p>
+
+<p><label for="password">Password</label><br/>
+<%%= password_field_tag 'password' %></p>
+
+<!-- Uncomment this if you want this functionality
+<p><label for="remember_me">Remember me:</label>
+<%%= check_box_tag 'remember_me' %></p>
+-->
+
+<p><%%= submit_tag 'Log in' %></p>
+<%% end -%>
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/migration.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/migration.rb
new file mode 100644 (file)
index 0000000..3f736b8
--- /dev/null
@@ -0,0 +1,18 @@
+class <%= migration_name %> < ActiveRecord::Migration
+  def self.up
+    create_table "<%= table_name %>", :force => true do |t|
+      t.column :login,                     :string
+      t.column :email,                     :string
+      t.column :crypted_password,          :string, :limit => 40
+      t.column :salt,                      :string, :limit => 40
+      t.column :created_at,                :datetime
+      t.column :updated_at,                :datetime
+      t.column :remember_token,            :string
+      t.column :remember_token_expires_at, :datetime
+    end
+  end
+
+  def self.down
+    drop_table "<%= table_name %>"
+  end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/model.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/model.rb
new file mode 100644 (file)
index 0000000..308618b
--- /dev/null
@@ -0,0 +1,64 @@
+require 'digest/sha1'
+class <%= class_name %> < ActiveRecord::Base
+  # Virtual attribute for the unencrypted password
+  attr_accessor :password
+
+  validates_presence_of     :login, :email
+  validates_presence_of     :password,                   :if => :password_required?
+  validates_presence_of     :password_confirmation,      :if => :password_required?
+  validates_length_of       :password, :within => 4..40, :if => :password_required?
+  validates_confirmation_of :password,                   :if => :password_required?
+  validates_length_of       :login,    :within => 3..40
+  validates_length_of       :email,    :within => 3..100
+  validates_uniqueness_of   :login, :email, :case_sensitive => false
+  before_save :encrypt_password
+
+  # Authenticates a user by their login name and unencrypted password.  Returns the user or nil.
+  def self.authenticate(login, password)
+    u = find_by_login(login) # need to get the salt
+    u && u.authenticated?(password) ? u : nil
+  end
+
+  # Encrypts some data with the salt.
+  def self.encrypt(password, salt)
+    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
+  end
+
+  # Encrypts the password with the user salt
+  def encrypt(password)
+    self.class.encrypt(password, salt)
+  end
+
+  def authenticated?(password)
+    crypted_password == encrypt(password)
+  end
+
+  def remember_token?
+    remember_token_expires_at && Time.now.utc < remember_token_expires_at 
+  end
+
+  # These create and unset the fields required for remembering users between browser closes
+  def remember_me
+    self.remember_token_expires_at = 2.weeks.from_now.utc
+    self.remember_token            = encrypt("#{email}--#{remember_token_expires_at}")
+    save(false)
+  end
+
+  def forget_me
+    self.remember_token_expires_at = nil
+    self.remember_token            = nil
+    save(false)
+  end
+
+  protected
+    # before filter 
+    def encrypt_password
+      return if password.blank?
+      self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record?
+      self.crypted_password = encrypt(password)
+    end
+    
+    def password_required?
+      crypted_password.blank? || !password.blank?
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/signup.rhtml b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/signup.rhtml
new file mode 100644 (file)
index 0000000..30d1053
--- /dev/null
@@ -0,0 +1,16 @@
+<%%= error_messages_for :<%= file_name %> %>
+<%% form_for :<%= file_name %> do |f| -%>
+<p><label for="login">Login</label><br/>
+<%%= f.text_field :login %></p>
+
+<p><label for="email">Email</label><br/>
+<%%= f.text_field :email %></p>
+
+<p><label for="password">Password</label><br/>
+<%%= f.password_field :password %></p>
+
+<p><label for="password_confirmation">Confirm Password</label><br/>
+<%%= f.password_field :password_confirmation %></p>
+
+<p><%%= submit_tag 'Sign up' %></p>
+<%% end -%>
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/unit_test.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated/templates/unit_test.rb
new file mode 100644 (file)
index 0000000..5aa0ab5
--- /dev/null
@@ -0,0 +1,75 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+  # Be sure to include AuthenticatedTestHelper in test/test_helper.rb instead.
+  # Then, you can remove it from this and the functional test.
+  include AuthenticatedTestHelper
+  fixtures :<%= table_name %>
+
+  def test_should_create_<%= file_name %>
+    assert_difference <%= class_name %>, :count do
+      <%= file_name %> = create_<%= file_name %>
+      assert !<%= file_name %>.new_record?, "#{<%= file_name %>.errors.full_messages.to_sentence}"
+    end
+  end
+
+  def test_should_require_login
+    assert_no_difference <%= class_name %>, :count do
+      u = create_<%= file_name %>(:login => nil)
+      assert u.errors.on(:login)
+    end
+  end
+
+  def test_should_require_password
+    assert_no_difference <%= class_name %>, :count do
+      u = create_<%= file_name %>(:password => nil)
+      assert u.errors.on(:password)
+    end
+  end
+
+  def test_should_require_password_confirmation
+    assert_no_difference <%= class_name %>, :count do
+      u = create_<%= file_name %>(:password_confirmation => nil)
+      assert u.errors.on(:password_confirmation)
+    end
+  end
+
+  def test_should_require_email
+    assert_no_difference <%= class_name %>, :count do
+      u = create_<%= file_name %>(:email => nil)
+      assert u.errors.on(:email)
+    end
+  end
+
+  def test_should_reset_password
+    <%= table_name %>(:quentin).update_attributes(:password => 'new password', :password_confirmation => 'new password')
+    assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin', 'new password')
+  end
+
+  def test_should_not_rehash_password
+    <%= table_name %>(:quentin).update_attributes(:login => 'quentin2')
+    assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin2', 'test')
+  end
+
+  def test_should_authenticate_<%= file_name %>
+    assert_equal <%= table_name %>(:quentin), <%= class_name %>.authenticate('quentin', 'test')
+  end
+
+  def test_should_set_remember_token
+    <%= table_name %>(:quentin).remember_me
+    assert_not_nil <%= table_name %>(:quentin).remember_token
+    assert_not_nil <%= table_name %>(:quentin).remember_token_expires_at
+  end
+
+  def test_should_unset_remember_token
+    <%= table_name %>(:quentin).remember_me
+    assert_not_nil <%= table_name %>(:quentin).remember_token
+    <%= table_name %>(:quentin).forget_me
+    assert_nil <%= table_name %>(:quentin).remember_token
+  end
+
+  protected
+    def create_<%= file_name %>(options = {})
+      <%= class_name %>.create({ :login => 'quire', :email => 'quire@example.com', :password => 'quire', :password_confirmation => 'quire' }.merge(options))
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/USAGE b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/USAGE
new file mode 100644 (file)
index 0000000..ba1676c
--- /dev/null
@@ -0,0 +1 @@
+./script/generate authenticated_mailer USERMODEL
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/authenticated_mailer_generator.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/authenticated_mailer_generator.rb
new file mode 100644 (file)
index 0000000..df2a94a
--- /dev/null
@@ -0,0 +1,27 @@
+class AuthenticatedMailerGenerator < Rails::Generator::NamedBase
+  def manifest
+    record do |m|
+      # Check for class naming collisions.
+      m.class_collisions class_path, "#{class_name}Notifier", "#{class_name}NotifierTest", "#{class_name}Observer"
+
+      # Controller, helper, views, and test directories.
+      m.directory File.join('app/models', class_path)
+      m.directory File.join('app/views', class_path, "#{file_name}_notifier")
+      m.directory File.join('test/unit', class_path)
+
+      %w( notifier observer ).each do |model_type|
+        m.template "#{model_type}.rb", File.join('app/models',
+                                             class_path,
+                                             "#{file_name}_#{model_type}.rb")
+      end
+      
+      m.template 'notifier_test.rb', File.join('test/unit', class_path, "#{file_name}_notifier_test.rb")
+
+      # Mailer templates
+      %w( activation signup_notification ).each do |action|
+        m.template "#{action}.rhtml",
+                   File.join('app/views', "#{file_name}_notifier", "#{action}.rhtml")
+      end
+    end
+  end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/activation.rhtml b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/activation.rhtml
new file mode 100644 (file)
index 0000000..2258e1f
--- /dev/null
@@ -0,0 +1,3 @@
+<%%= @<%= file_name %>.login %>, your account has been activated.  You may now start adding your plugins:
+
+  <%%= @url %>
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier.rb
new file mode 100644 (file)
index 0000000..df52af8
--- /dev/null
@@ -0,0 +1,22 @@
+class <%= class_name %>Notifier < ActionMailer::Base
+  def signup_notification(<%= file_name %>)
+    setup_email(<%= file_name %>)
+    @subject    += 'Please activate your new account'
+    @body[:url]  = "http://YOURSITE/account/activate/#{<%= file_name %>.activation_code}"
+  end
+  
+  def activation(<%= file_name %>)
+    setup_email(<%= file_name %>)
+    @subject    += 'Your account has been activated!'
+    @body[:url]  = "http://YOURSITE/"
+  end
+  
+  protected
+  def setup_email(<%= file_name %>)
+    @recipients  = "#{<%= file_name %>.email}"
+    @from        = "ADMINEMAIL"
+    @subject     = "[YOURSITE] "
+    @sent_on     = Time.now
+    @body[:<%= file_name %>] = <%= file_name %>
+  end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier_test.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/notifier_test.rb
new file mode 100644 (file)
index 0000000..20aa05d
--- /dev/null
@@ -0,0 +1,27 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>_notifier'
+
+class <%= class_name %>NotifierTest < Test::Unit::TestCase
+  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+  CHARSET = "utf-8"
+
+  include ActionMailer::Quoting
+
+  def setup
+    ActionMailer::Base.delivery_method = :test
+    ActionMailer::Base.perform_deliveries = true
+    ActionMailer::Base.deliveries = []
+
+    @expected = TMail::Mail.new
+    @expected.set_content_type "text", "plain", { "charset" => CHARSET }
+  end
+
+  private
+    def read_fixture(action)
+      IO.readlines("#{FIXTURES_PATH}/<%= file_name %>_notifier/#{action}")
+    end
+
+    def encode(subject)
+      quoted_printable(subject, CHARSET)
+    end
+end
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/observer.rb b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/observer.rb
new file mode 100644 (file)
index 0000000..6ade4f3
--- /dev/null
@@ -0,0 +1,9 @@
+class <%= class_name %>Observer < ActiveRecord::Observer
+  def after_create(<%= file_name %>)
+    <%= class_name %>Notifier.deliver_signup_notification(<%= file_name %>)
+  end
+
+  def after_save(<%= file_name %>)
+    <%= class_name %>Notifier.deliver_activation(<%= file_name %>) if <%= file_name %>.recently_activated?
+  end
+end
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/signup_notification.rhtml b/vendor/plugins/acts_as_authenticated/generators/authenticated_mailer/templates/signup_notification.rhtml
new file mode 100644 (file)
index 0000000..6b0d675
--- /dev/null
@@ -0,0 +1,8 @@
+Your account has been created.
+
+  Username: <%%= @<%= file_name %>.login %>
+  Password: <%%= @<%= file_name %>.password %>
+
+Visit this url to activate your account:
+
+  <%%= @url %>
\ No newline at end of file
diff --git a/vendor/plugins/acts_as_authenticated/install.rb b/vendor/plugins/acts_as_authenticated/install.rb
new file mode 100644 (file)
index 0000000..a63be40
--- /dev/null
@@ -0,0 +1 @@
+puts IO.read(File.join(File.dirname(__FILE__), 'README'))
\ No newline at end of file
diff --git a/vendor/plugins/gruff/MIT-LICENSE b/vendor/plugins/gruff/MIT-LICENSE
new file mode 100644 (file)
index 0000000..8d1b480
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2006 Geoffrey Grosenbach
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/plugins/gruff/README b/vendor/plugins/gruff/README
new file mode 100644 (file)
index 0000000..db98db5
--- /dev/null
@@ -0,0 +1,15 @@
+== Gruff Plugin
+
+Make pretty graphs.
+
+Also has a "gruff" generator for copying a controller and functional test to your app.
+
+  ./script/generate gruff Reports
+
+See examples at http://nubyonrails.com/pages/gruff
+
+== Author
+
+Geoffrey Grosenbach boss@topfunky.com http://nubyonrails.com
+
+
diff --git a/vendor/plugins/gruff/Rakefile b/vendor/plugins/gruff/Rakefile
new file mode 100644 (file)
index 0000000..d3456e4
--- /dev/null
@@ -0,0 +1,23 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the plugin.'
+Rake::TestTask.new(:test) do |t|
+  t.libs << 'lib'
+  t.pattern = 'test/**/test_*.rb'
+  t.verbose = true
+end
+
+desc 'Generate documentation for the plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title    = 'CalendarHelper'
+  rdoc.options << '--line-numbers' << '--inline-source'
+  rdoc.rdoc_files.include('README')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
diff --git a/vendor/plugins/gruff/about.yml b/vendor/plugins/gruff/about.yml
new file mode 100644 (file)
index 0000000..874f85f
--- /dev/null
@@ -0,0 +1,7 @@
+author: topfunky
+summary: Make graphs. Requires RMagick. Includes a generator for a graphics controller, and tests.
+homepage: http://nubyonrails.com/pages/gruff
+plugin: http://topfunky.net/svn/plugins/gruff
+license: MIT
+version: 0.1.2
+rails_version: 1.0+
diff --git a/vendor/plugins/gruff/generators/gruff/gruff_generator.rb b/vendor/plugins/gruff/generators/gruff/gruff_generator.rb
new file mode 100644 (file)
index 0000000..337f58e
--- /dev/null
@@ -0,0 +1,63 @@
+class GruffGenerator < Rails::Generator::NamedBase
+
+  attr_reader   :controller_name,
+                :controller_class_path,
+                :controller_file_path,
+                :controller_class_nesting,
+                :controller_class_nesting_depth,
+                :controller_class_name,
+                :controller_singular_name,
+                :controller_plural_name,
+                :parent_folder_for_require
+  alias_method  :controller_file_name,  :controller_singular_name
+  alias_method  :controller_table_name, :controller_plural_name
+
+  def initialize(runtime_args, runtime_options = {})
+    super
+
+    # Take controller name from the next argument.
+    @controller_name = runtime_args.shift
+
+    base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@controller_name)
+    @controller_class_name_without_nesting, @controller_singular_name, @controller_plural_name = inflect_names(base_name)
+
+    if @controller_class_nesting.empty?
+      @controller_class_name = @controller_class_name_without_nesting
+    else
+      @controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
+    end    
+  end
+
+  def manifest
+    record do |m|
+      # Check for class naming collisions.
+      m.class_collisions controller_class_path, "#{controller_class_name}Controller",
+                                                "#{controller_class_name}ControllerTest"
+
+      # Controller, helper, views, and test directories.
+      m.directory File.join('app/controllers', controller_class_path)
+      m.directory File.join('test/functional', controller_class_path)
+
+      m.template 'controller.rb',
+                  File.join('app/controllers',
+                            controller_class_path,
+                            "#{controller_file_name}_controller.rb")
+
+      # For some reason this doesn't take effect if done in initialize()
+      @parent_folder_for_require = @controller_class_path.join('/').gsub(%r%app/controllers/?%, '')
+      @parent_folder_for_require += @parent_folder_for_require.blank? ? '' : '/'
+
+      m.template 'functional_test.rb',
+                  File.join('test/functional',
+                            controller_class_path,
+                            "#{controller_file_name}_controller_test.rb")
+
+    end
+  end
+
+  protected
+    # Override with your own usage banner.
+    def banner
+      "Usage: #{$0} gruff ControllerName"
+    end
+end
diff --git a/vendor/plugins/gruff/generators/gruff/templates/controller.rb b/vendor/plugins/gruff/generators/gruff/templates/controller.rb
new file mode 100644 (file)
index 0000000..b8f90dd
--- /dev/null
@@ -0,0 +1,32 @@
+class <%= controller_class_name %>Controller < ApplicationController
+
+  # To make caching easier, add a line like this to config/routes.rb:
+  # map.graph "graph/:action/:id/image.png", :controller => "graph"
+  #
+  # Then reference it with the named route:
+  #   image_tag graph_url(:action => 'show', :id => 42)
+
+  def show
+    g = Gruff::Line.new
+    # Uncomment to use your own theme or font
+    # See http://colourlovers.com or http://www.firewheeldesign.com/widgets/ for color ideas
+#     g.theme = {
+#       :colors => ['#663366', '#cccc99', '#cc6633', '#cc9966', '#99cc99'],
+#       :marker_color => 'white',
+#       :background_colors => ['black', '#333333']
+#     }
+#     g.font = File.expand_path('artwork/fonts/VeraBd.ttf', RAILS_ROOT)
+
+    g.title = "Gruff-o-Rama"
+    
+    g.data("Apples", [1, 2, 3, 4, 4, 3])
+    g.data("Oranges", [4, 8, 7, 9, 8, 9])
+    g.data("Watermelon", [2, 3, 1, 5, 6, 8])
+    g.data("Peaches", [9, 9, 10, 8, 7, 9])
+
+    g.labels = {0 => '2004', 2 => '2005', 4 => '2006'}
+
+    send_data(g.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "gruff.png")
+  end
+
+end
diff --git a/vendor/plugins/gruff/generators/gruff/templates/functional_test.rb b/vendor/plugins/gruff/generators/gruff/templates/functional_test.rb
new file mode 100644 (file)
index 0000000..c1f0818
--- /dev/null
@@ -0,0 +1,24 @@
+require File.dirname(__FILE__) + '<%= '/..' * controller_class_name.split("::").length %>/test_helper'
+require '<%= parent_folder_for_require %><%= controller_file_name %>_controller'
+
+# Re-raise errors caught by the controller.
+class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end
+
+class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase
+
+  #fixtures :data
+
+  def setup
+    @controller = <%= controller_class_name %>Controller.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+  end
+
+  # TODO Replace this with your actual tests
+  def test_show
+    get :show
+    assert_response :success
+    assert_equal 'image/png', @response.headers['Content-Type']
+  end
+  
+end
diff --git a/vendor/plugins/gruff/lib/gruff.rb b/vendor/plugins/gruff/lib/gruff.rb
new file mode 100644 (file)
index 0000000..e281916
--- /dev/null
@@ -0,0 +1,24 @@
+# Extra full path added to fix loading errors on some installations.
+
+%w(
+  base
+  area
+  bar
+  line
+  pie
+  spider
+  net
+  stacked_bar
+  side_stacked_bar
+  side_bar
+  accumulator_bar
+
+  scene
+
+  mini/legend
+  mini/bar
+  mini/pie
+  mini/side_bar
+).each do |filename|
+  require File.dirname(__FILE__) + "/gruff/#{filename}"
+end
diff --git a/vendor/plugins/gruff/lib/gruff/accumulator_bar.rb b/vendor/plugins/gruff/lib/gruff/accumulator_bar.rb
new file mode 100644 (file)
index 0000000..310e193
--- /dev/null
@@ -0,0 +1,27 @@
+require File.dirname(__FILE__) + '/base'
+
+##
+# A special bar graph that shows a single dataset as a set of
+# stacked bars. The bottom bar shows the running total and 
+# the top bar shows the new value being added to the array.
+
+class Gruff::AccumulatorBar < Gruff::StackedBar
+
+  def draw
+    raise(Gruff::IncorrectNumberOfDatasetsException) unless @data.length == 1
+    
+    accumulator_array = []
+    index = 0
+
+    increment_array = @data.first[DATA_VALUES_INDEX].inject([]) {|memo, value| 
+        memo << ((index > 0) ? (value + memo.max) : value)
+        accumulator_array << memo[index] - value
+        index += 1
+        memo
+      }
+    data "Accumulator", accumulator_array
+    
+    super
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/area.rb b/vendor/plugins/gruff/lib/gruff/area.rb
new file mode 100644 (file)
index 0000000..323aff1
--- /dev/null
@@ -0,0 +1,58 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Area < Gruff::Base
+
+  def draw
+    super
+
+    return unless @has_data
+
+    @x_increment = @graph_width / (@column_count - 1).to_f
+    @d = @d.stroke 'transparent'
+
+    @norm_data.each do |data_row|
+      poly_points = Array.new
+      prev_x = prev_y = 0.0
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, index|
+        # Use incremented x and scaled y
+        new_x = @graph_left + (@x_increment * index)
+        new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+        if prev_x > 0 and prev_y > 0 then
+          poly_points << new_x
+          poly_points << new_y
+          
+          #@d = @d.polyline(prev_x, prev_y, new_x, new_y)
+        else
+          poly_points << @graph_left
+          poly_points << @graph_bottom - 1
+          poly_points << new_x
+          poly_points << new_y
+          
+          #@d = @d.polyline(@graph_left, @graph_bottom, new_x, new_y)
+        end
+
+        draw_label(new_x, index)
+
+        prev_x = new_x
+        prev_y = new_y
+      end
+
+      # Add closing points, draw polygon
+      poly_points << @graph_right
+      poly_points << @graph_bottom - 1
+      poly_points << @graph_left
+      poly_points << @graph_bottom - 1
+
+      @d = @d.polyline(*poly_points)
+
+    end
+
+    @d.draw(@base_image)
+  end
+   
+end
diff --git a/vendor/plugins/gruff/lib/gruff/bar.rb b/vendor/plugins/gruff/lib/gruff/bar.rb
new file mode 100644 (file)
index 0000000..89eeac7
--- /dev/null
@@ -0,0 +1,84 @@
+
+require File.dirname(__FILE__) + '/base'
+require File.dirname(__FILE__) + '/bar_conversion'
+
+class Gruff::Bar < Gruff::Base
+
+  def draw
+    # Labels will be centered over the left of the bar if
+    # there are more labels than columns. This is basically the same 
+    # as where it would be for a line graph.
+    @center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
+    
+    super
+    return unless @has_data
+
+    draw_bars
+  end
+
+protected
+
+  def draw_bars
+    # Setup spacing.
+    #
+    # Columns sit side-by-side.
+    spacing_factor = 0.9 # space between the bars
+    @bar_width = @graph_width / (@column_count * @data.length).to_f
+
+    @d = @d.stroke_opacity 0.0
+
+    # Setup the BarConversion Object
+    conversion = Gruff::BarConversion.new()
+    conversion.graph_height = @graph_height
+    conversion.graph_top = @graph_top
+
+    # Set up the right mode [1,2,3] see BarConversion for further explanation
+    if @minimum_value >= 0 then
+      # all bars go from zero to positiv
+      conversion.mode = 1
+    else
+      # all bars go from 0 to negativ
+      if @maximum_value <= 0 then
+        conversion.mode = 2
+      else
+        # bars either go from zero to negativ or to positiv
+        conversion.mode = 3
+        conversion.spread = @spread
+        conversion.minimum_value = @minimum_value
+        conversion.zero = -@minimum_value/@spread
+      end
+    end
+
+    # iterate over all normalised data
+    @norm_data.each_with_index do |data_row, row_index|
+
+      data_row[1].each_with_index do |data_point, point_index|
+        # Use incremented x and scaled y
+        # x
+        left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+        right_x = left_x + @bar_width * spacing_factor
+        # y
+        conv = []
+        conversion.getLeftYRightYscaled( data_point, conv )
+
+        # create new bar
+        @d = @d.fill data_row[DATA_COLOR_INDEX]
+        @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
+
+        # Calculate center based on bar_width and current row
+        label_center = @graph_left + 
+                      (@data.length * @bar_width * point_index) + 
+                      (@data.length * @bar_width / 2.0)
+        # Subtract half a bar width to center left if requested
+        draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index)
+      end
+
+    end
+
+    # Draw the last label if requested
+    draw_label(@graph_right, @column_count) if @center_labels_over_point
+
+    @d.draw(@base_image)
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/bar_conversion.rb b/vendor/plugins/gruff/lib/gruff/bar_conversion.rb
new file mode 100644 (file)
index 0000000..880daca
--- /dev/null
@@ -0,0 +1,46 @@
+##
+# Original Author: David Stokar
+#
+#      This class perfoms the y coordinats conversion for the bar class.
+#
+#      There are three cases: 
+#
+#   1. Bars all go from zero in positive direction
+#              2. Bars all go from zero to negative direction  
+#              3. Bars either go from zero to positive or from zero to negative
+#
+class Gruff::BarConversion
+       attr_writer :mode
+       attr_writer :zero
+       attr_writer :graph_top
+       attr_writer :graph_height
+       attr_writer :minimum_value
+       attr_writer :spread
+       
+       def getLeftYRightYscaled( data_point, result )
+               case @mode
+               when 1 then # Case one
+                       # minimum value >= 0 ( only positiv values )
+      result[0] = @graph_top + @graph_height*(1 - data_point) + 1
+               result[1] = @graph_top + @graph_height - 1
+               when 2 then  # Case two
+                       # only negativ values
+               result[0] = @graph_top + 1
+               result[1] = @graph_top + @graph_height*(1 - data_point) - 1
+               when 3 then # Case three
+                       # positiv and negativ values
+       val = data_point-@minimum_value/@spread
+       if ( data_point >= @zero ) then
+               result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+               result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+       else
+                               result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+               result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+       end
+               else
+                       result[0] = 0.0
+                       result[1] = 0.0
+               end                             
+       end     
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/base.rb b/vendor/plugins/gruff/lib/gruff/base.rb
new file mode 100644 (file)
index 0000000..88cec94
--- /dev/null
@@ -0,0 +1,1051 @@
+#
+# = Gruff. Graphs.
+#
+# Author:: Geoffrey Grosenbach boss@topfunky.com
+#
+# Originally Created:: October 23, 2005
+#
+# Extra thanks to Tim Hunter for writing RMagick, 
+# and also contributions by
+# Jarkko Laine, Mike Perham, Andreas Schwarz, 
+# Alun Eyre, Guillaume Theoret, David Stokar, 
+# Paul Rogers, Dave Woodward, Frank Oxener,
+# Kevin Clark, Cies Breijs, Richard Cowin,
+# and a cast of thousands.
+#
+
+require 'rubygems'
+require 'RMagick'
+require File.dirname(__FILE__) + '/deprecated'
+
+module Gruff
+  
+  VERSION = '0.2.9'
+  
+  class Base
+  
+    include Magick
+    include Deprecated
+
+    # Draw extra lines showing where the margins and text centers are
+    DEBUG = false
+
+    # Used for navigating the array of data to plot
+    DATA_LABEL_INDEX = 0
+    DATA_VALUES_INDEX = 1
+    DATA_COLOR_INDEX = 2
+
+    # Blank space around the edges of the graph
+    TOP_MARGIN = BOTTOM_MARGIN = RIGHT_MARGIN = LEFT_MARGIN = 20.0
+    
+    # Space around text elements. Mostly used for vertical spacing
+    LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
+
+    DEFAULT_TARGET_WIDTH = 800
+    
+    # A hash of names for the individual columns, where the key is the array index for the column this label represents.
+    #
+    # Not all columns need to be named.
+    #
+    # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
+    attr_accessor :labels
+
+    # Used internally for spacing.
+    #
+    # By default, labels are centered over the point they represent.
+    attr_accessor :center_labels_over_point
+
+    # Used internally for horizontal graph types.
+    attr_accessor :has_left_labels
+
+    # A label for the bottom of the graph
+    attr_accessor :x_axis_label
+    
+    # A label for the left side of the graph
+    attr_accessor :y_axis_label
+
+    # attr_accessor :x_axis_increment
+    
+    # Manually set increment of the horizontal marking lines
+    attr_accessor :y_axis_increment
+
+    # Get or set the list of colors that will be used to draw the bars or lines.
+    attr_accessor :colors
+
+    # The large title of the graph displayed at the top
+    attr_accessor :title
+
+    # Font used for titles, labels, etc. Works best if you provide the full path to the TTF font file.
+    # RMagick must be built with the Freetype libraries for this to work properly.
+    #
+    # Tries to find Bitstream Vera (Vera.ttf) in the location specified by
+    # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise.
+    #
+    # The font= method below fulfills the role of the writer, so we only need 
+    # a reader here.
+    attr_reader :font
+
+    attr_accessor :font_color
+
+    # Hide various elements
+    attr_accessor :hide_line_markers, :hide_legend, :hide_title, :hide_line_numbers
+
+    # Message shown when there is no data. Fits up to 20 characters. Defaults to "No Data."
+    attr_accessor :no_data_message
+
+    # The font size of the large title at the top of the graph
+    attr_accessor :title_font_size
+
+    # Optionally set the size of the font. Based on an 800x600px graph. Default is 20.
+    #
+    # Will be scaled down if graph is smaller than 800px wide.
+    attr_accessor :legend_font_size
+
+    # The font size of the labels around the graph
+    attr_accessor :marker_font_size
+    
+    # The color of the auxiliary lines
+    attr_accessor :marker_color
+
+    # The number of horizontal lines shown for reference
+    attr_accessor :marker_count
+    
+
+    # You can manually set a minimum value instead of having the values guessed for you.
+    #
+    # Set it after you have given all your data to the graph object.
+    attr_accessor :minimum_value
+
+    # You can manually set a maximum value, such as a percentage-based graph that always goes to 100.
+    #
+    # If you use this, you must set it after you have given all your data to the graph object.
+    attr_accessor :maximum_value
+    
+    # Set to false if you don't want the data to be sorted with largest avg values at the back.
+    attr_accessor :sort
+    
+    # Experimental
+    attr_accessor :additional_line_values
+    
+    # Experimental
+    attr_accessor :stacked
+    
+    
+    # Optionally set the size of the colored box by each item in the legend. Default is 20.0
+    #
+    # Will be scaled down if graph is smaller than 800px wide.
+    attr_accessor :legend_box_size
+
+
+    # If one numerical argument is given, the graph is drawn at 4/3 ratio according to the given width (800 results in 800x600, 400 gives 400x300, etc.).
+    #
+    # Or, send a geometry string for other ratios ('800x400', '400x225'). 
+    #
+    # Looks for Bitstream Vera as the default font. Expects an environment var of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.)
+    def initialize(target_width=DEFAULT_TARGET_WIDTH)
+
+      if not Numeric === target_width
+        geometric_width, geometric_height = target_width.split('x')
+        @columns = geometric_width.to_f
+        @rows = geometric_height.to_f
+      else
+        @columns = target_width.to_f
+        @rows = target_width.to_f * 0.75        
+      end
+
+      initialize_ivars
+
+      reset_themes
+      theme_keynote
+    end
+
+    ##
+    # Set instance variables for this object.
+    #
+    # Subclasses can override this, call super, then set values separately.
+    #
+    # This makes it possible to set defaults in a subclass but still allow
+    # developers to change this values in their program.
+    
+    def initialize_ivars
+      # Internal for calculations
+      @raw_columns = 800.0
+      @raw_rows = 800.0 * (@rows/@columns)
+      @column_count = 0
+      @marker_count = nil
+      @maximum_value = @minimum_value = nil
+      @has_data = false
+      @data = Array.new
+      @labels = Hash.new
+      @labels_seen = Hash.new
+      @sort = true
+      @title = nil
+
+      @scale = @columns / @raw_columns
+
+      vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
+      @font = File.exists?(vera_font_path) ? vera_font_path : nil
+
+      @marker_font_size = 21.0
+      @legend_font_size = 20.0
+      @title_font_size = 36.0
+      
+      @legend_box_size = 20.0
+
+      @no_data_message = "No Data"
+
+      @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
+      @center_labels_over_point = true
+      @has_left_labels = false
+
+      @additional_line_values = []      
+      @additional_line_colors = []
+      @theme_options = {}
+      
+      @x_axis_label = @y_axis_label = nil
+      @y_axis_increment = nil
+      @stacked = nil
+      @norm_data = nil
+    end
+    
+    def font=(font_path)
+      @font = font_path
+      @d.font = @font
+    end
+
+    # Add a color to the list of available colors for lines.
+    #
+    # Example: 
+    #  add_color('#c0e9d3')
+    def add_color(colorname)
+      @colors << colorname
+    end
+
+
+    # Replace the entire color list with a new array of colors. You need to have one more color
+    # than the number of datasets you intend to draw. Also aliased as the colors= setter method.
+    #
+    # Example: 
+    #  replace_colors('#cc99cc', '#d9e043', '#34d8a2')
+    def replace_colors(color_list=[])
+      @colors = color_list
+    end
+
+
+    # You can set a theme manually. Assign a hash to this method before you send your data.
+    #
+    #  graph.theme = {
+    #    :colors => %w(orange purple green white red),
+    #    :marker_color => 'blue',
+    #    :background_colors => %w(black grey)
+    #  }
+    #
+    # :background_image => 'squirrel.png' is also possible.
+    #
+    # (Or hopefully something better looking than that.)
+    #
+    def theme=(options)
+      reset_themes()
+      
+      defaults = {
+        :colors => ['black', 'white'],
+        :additional_line_colors => [],
+        :marker_color => 'white',
+        :font_color => 'black',
+        :background_colors => nil,
+        :background_image => nil
+      }
+      @theme_options = defaults.merge options
+
+      @colors = @theme_options[:colors]
+      @marker_color = @theme_options[:marker_color]
+      @font_color = @theme_options[:font_color] || @marker_color
+      @additional_line_colors = @theme_options[:additional_line_colors]
+      
+      render_background
+    end
+    
+    # A color scheme similar to the popular presentation software.
+    def theme_keynote
+      # Colors
+      @blue = '#6886B4'
+      @yellow = '#FDD84E'
+      @green = '#72AE6E'
+      @red = '#D1695E'
+      @purple = '#8A6EAF'
+      @orange = '#EFAA43'
+      @white = 'white'
+      @colors = [@yellow, @blue, @green, @red, @purple, @orange, @white]
+
+      self.theme = {
+        :colors => @colors,
+        :marker_color => 'white',
+        :font_color => 'white',
+        :background_colors => ['black', '#4a465a']
+      }
+    end
+    
+    # A color scheme plucked from the colors on the popular usability blog.
+    def theme_37signals
+      # Colors
+      @green = '#339933'
+      @purple = '#cc99cc'
+      @blue = '#336699'
+      @yellow = '#FFF804'
+      @red = '#ff0000'
+      @orange = '#cf5910'
+      @black = 'black'
+      @colors = [@yellow, @blue, @green, @red, @purple, @orange, @black]
+
+      self.theme = {
+        :colors => @colors,
+        :marker_color => 'black',
+        :font_color => 'black',
+        :background_colors => ['#d1edf5', 'white']
+      }
+    end
+
+    # A color scheme from the colors used on the 2005 Rails keynote presentation at RubyConf.
+    def theme_rails_keynote
+      # Colors
+      @green = '#00ff00'
+      @grey = '#333333'
+      @orange = '#ff5d00'
+      @red = '#f61100'
+      @white = 'white'
+      @light_grey = '#999999'
+      @black = 'black'
+      @colors = [@green, @grey, @orange, @red, @white, @light_grey, @black]
+      
+      self.theme = {
+        :colors => @colors,
+        :marker_color => 'white',
+        :font_color => 'white',
+        :background_colors => ['#0083a3', '#0083a3']
+      }
+    end
+
+    # A color scheme similar to that used on the popular podcast site.
+    def theme_odeo
+      # Colors
+      @grey = '#202020'
+      @white = 'white'
+      @dark_pink = '#a21764'
+      @green = '#8ab438'
+      @light_grey = '#999999'
+      @dark_blue = '#3a5b87'
+      @black = 'black'
+      @colors = [@grey, @white, @dark_blue, @dark_pink, @green, @light_grey, @black]
+      
+      self.theme = {
+        :colors => @colors,
+        :marker_color => 'white',
+        :font_color => 'white',
+        :background_colors => ['#ff47a4', '#ff1f81']
+      }
+    end
+
+    # A pastel theme
+    def theme_pastel
+      # Colors
+      @colors = [
+          '#a9dada', # blue
+          '#aedaa9', # green
+          '#daaea9', # peach
+          '#dadaa9', # yellow
+          '#a9a9da', # dk purple
+          '#daaeda', # purple
+          '#dadada' # grey
+        ]
+      
+      self.theme = {
+        :colors => @colors,
+        :marker_color => '#aea9a9', # Grey
+        :font_color => 'black',
+        :background_colors => 'white'
+      }
+    end
+
+    # A greyscale theme
+    def theme_greyscale
+      # Colors
+      @colors = [
+          '#282828', # 
+          '#383838', # 
+          '#686868', # 
+          '#989898', # 
+          '#c8c8c8', # 
+          '#e8e8e8', # 
+        ]
+      
+      self.theme = {
+        :colors => @colors,
+        :marker_color => '#aea9a9', # Grey
+        :font_color => 'black',
+        :background_colors => 'white'
+      }
+    end
+
+
+    # Parameters are an array where the first element is the name of the dataset
+    # and the value is an array of values to plot.
+    #
+    # Can be called multiple times with different datasets for a multi-valued graph.
+    #
+    # If the color argument is nil, the next color from the default theme will be used.
+    #
+    # NOTE: If you want to use a preset theme, you must set it before calling data().
+    #
+    # Example:
+    #
+    #  data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
+    #
+    def data(name, data_points=[], color=nil)
+      data_points = Array(data_points) # make sure it's an array
+      @data << [name, data_points, (color || increment_color)]
+      # Set column count if this is larger than previous counts
+      @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
+
+      # Pre-normalize
+      data_points.each_with_index do |data_point, index|
+        next if data_point.nil?
+        
+        # Setup max/min so spread starts at the low end of the data points
+        if @maximum_value.nil? && @minimum_value.nil?
+          @maximum_value = @minimum_value = data_point
+        end
+
+        # TODO Doesn't work with stacked bar graphs
+        # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
+        @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
+        @has_data = true if @maximum_value > 0
+        
+        @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
+         @has_data = true if @minimum_value < 0
+      end
+    end
+
+    # Writes the graph to a file. Defaults to 'graph.png'
+    #
+    # Example: write('graphs/my_pretty_graph.png')
+    def write(filename="graph.png")
+      draw()
+      @base_image.write(filename)
+    end
+
+    # Return the graph as a rendered binary blob.
+    def to_blob(fileformat='PNG')
+      draw()
+      return @base_image.to_blob do
+         self.format = fileformat
+      end
+    end
+
+protected
+
+    # Overridden by subclasses to do the actual plotting of the graph.
+    #
+    # Subclasses should start by calling super() for this method.
+    def draw
+      make_stacked if @stacked
+      setup_drawing
+      
+      debug {
+        # Outer margin
+        @d.rectangle( LEFT_MARGIN, TOP_MARGIN, 
+                            @raw_columns - RIGHT_MARGIN, @raw_rows - BOTTOM_MARGIN)
+        # Graph area box
+        @d.rectangle( @graph_left, @graph_top, @graph_right, @graph_bottom)
+      }
+    end
+
+    ##
+    # Calculates size of drawable area and draws the decorations.
+    #
+    # * line markers
+    # * legend
+    # * title
+    
+    def setup_drawing
+      # Maybe should be done in one of the following functions for more granularity.
+      unless @has_data
+        draw_no_data()
+        return
+      end
+      
+      normalize()
+      setup_graph_measurements()
+      sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
+      
+      draw_legend()
+      draw_line_markers()
+      draw_axis_labels()
+      draw_title
+    end
+
+    # Make copy of data with values scaled between 0-100
+    def normalize(force=false)
+      if @norm_data.nil? || force
+        @norm_data = []
+        return unless @has_data
+                
+        calculate_spread
+        
+        @data.each do |data_row|
+          norm_data_points = []
+          data_row[DATA_VALUES_INDEX].each do |data_point|
+            if data_point.nil?
+              norm_data_points << nil
+            else
+              norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
+            end
+          end
+          @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
+        end
+      end
+    end
+
+    def calculate_spread
+      @spread = @maximum_value.to_f - @minimum_value.to_f
+      @spread = @spread > 0 ? @spread : 1
+    end
+
+    ##
+    # Calculates size of drawable area, general font dimensions, etc.
+    
+    def setup_graph_measurements
+      @marker_caps_height = calculate_caps_height(@marker_font_size)
+      @title_caps_height = calculate_caps_height(@title_font_size)
+      @legend_caps_height = calculate_caps_height(@legend_font_size)
+      
+      if @hide_line_markers
+        (@graph_left, 
+         @graph_right_margin, 
+         @graph_bottom_margin) = [LEFT_MARGIN, RIGHT_MARGIN, BOTTOM_MARGIN]
+      else
+        longest_left_label_width = 0
+        if @has_left_labels
+          longest_left_label_width =  calculate_width(@marker_font_size,
+                                      labels.values.inject('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }) * 1.25
+        else
+          longest_left_label_width = calculate_width(@marker_font_size, 
+                          label(@maximum_value.to_f))
+        end
+      
+        # Shift graph if left line numbers are hidden
+        line_number_width = @hide_line_numbers && !@has_left_labels ? 
+                              0.0 : 
+                              (longest_left_label_width + LABEL_MARGIN * 2)
+
+        @graph_left = LEFT_MARGIN + 
+                      line_number_width + 
+                      (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
+        # Make space for half the width of the rightmost column label.
+        # Might be greater than the number of columns if between-style bar markers are used.
+        last_label = @labels.keys.sort.last.to_i
+        extra_room_for_long_label = (last_label >= (@column_count-1) && @center_labels_over_point) ?
+          calculate_width(@marker_font_size, @labels[last_label])/2.0 :
+          0
+        @graph_right_margin =   RIGHT_MARGIN + extra_room_for_long_label
+                                
+        @graph_bottom_margin =  BOTTOM_MARGIN + 
+                                @marker_caps_height + LABEL_MARGIN
+      end
+
+      @graph_right = @raw_columns - @graph_right_margin
+      @graph_width = @raw_columns - @graph_left - @graph_right_margin
+
+      # When @hide title, leave a TITLE_MARGIN space for aesthetics.
+      # Same with @hide_legend
+      @graph_top = TOP_MARGIN + 
+                    (@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
+                    (@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
+
+      @graph_bottom = @raw_rows - @graph_bottom_margin -
+                      (@x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN)
+      
+      @graph_height = @graph_bottom - @graph_top
+    end
+
+    # Draw the optional labels for the x axis and y axis.
+    def draw_axis_labels
+      unless @x_axis_label.nil?
+        # X Axis
+        # Centered vertically and horizontally by setting the
+        # height to 1.0 and the width to the width of the graph.
+        x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
+
+        # TODO Center between graph area
+        @d.fill = @font_color
+        @d.font = @font if @font
+        @d.stroke('transparent')
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = NorthGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          @raw_columns, 1.0, 
+                          0.0, x_axis_label_y_coordinate, 
+                          @x_axis_label, @scale)
+        debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
+      end
+
+      unless @y_axis_label.nil?
+        # Y Axis, rotated vertically
+        @d.rotation = 90.0
+        @d.gravity = CenterGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          1.0, @raw_rows,
+                          LEFT_MARGIN + @marker_caps_height / 2.0, 0.0, 
+                          @y_axis_label, @scale)
+        @d.rotation = -90.0
+      end
+    end
+
+    # Draws horizontal background lines and labels
+    def draw_line_markers
+      return if @hide_line_markers
+      
+      @d = @d.stroke_antialias false
+            
+      if @y_axis_increment.nil?
+        # Try to use a number of horizontal lines that will come out even.
+        #
+        # TODO Do the same for larger numbers...100, 75, 50, 25
+        if @marker_count.nil?
+          (3..7).each do |lines|
+            if @spread % lines == 0.0
+              @marker_count = lines
+              break
+            end
+          end
+          @marker_count ||= 4
+        end
+        @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
+      else
+        # TODO Make this work for negative values
+        @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
+        @minimum_value = @minimum_value.floor
+        calculate_spread
+        normalize(true)
+        
+        @marker_count = (@spread / @y_axis_increment).to_i
+        @increment = @y_axis_increment
+      end
+      @increment_scaled = @graph_height.to_f / (@spread / @increment)
+
+      # Draw horizontal line markers and annotate with numbers
+      (0..@marker_count).each do |index|
+        y = @graph_top + @graph_height - index.to_f * @increment_scaled
+        
+        @d = @d.stroke(@marker_color)
+        @d = @d.stroke_width 1
+        @d = @d.line(@graph_left, y, @graph_right, y)
+
+        marker_label = index * @increment + @minimum_value.to_f
+
+        unless @hide_line_numbers
+          @d.fill = @font_color
+          @d.font = @font if @font
+          @d.stroke('transparent')
+          @d.pointsize = scale_fontsize(@marker_font_size)
+          @d.gravity = EastGravity
+        
+          # Vertically center with 1.0 for the height
+          @d = @d.annotate_scaled( @base_image, 
+                            @graph_left - LABEL_MARGIN, 1.0,
+                            0.0, y,
+                            label(marker_label), @scale)
+        end
+      end
+      
+      # # Submitted by a contibutor...the utility escapes me
+      # i = 0
+      # @additional_line_values.each do |value|
+      #   @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
+      # 
+      #   y = @graph_top + @graph_height - @increment_scaled
+      # 
+      #   @d = @d.stroke(@additional_line_colors[i])
+      #   @d = @d.line(@graph_left, y, @graph_right, y)
+      # 
+      # 
+      #   @d.fill = @additional_line_colors[i]
+      #   @d.font = @font if @font
+      #   @d.stroke('transparent')
+      #   @d.pointsize = scale_fontsize(@marker_font_size)
+      #   @d.gravity = EastGravity
+      #   @d = @d.annotate_scaled( @base_image, 
+      #                     100, 20,
+      #                     -10, y - (@marker_font_size/2.0), 
+      #                     "", @scale)
+      #   i += 1   
+      # end
+      
+      @d = @d.stroke_antialias true
+    end
+
+    # Draws a legend with the names of the datasets 
+    # matched to the colors used to draw them.
+    def draw_legend
+      return if @hide_legend
+
+      @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
+
+      legend_square_width = @legend_box_size # small square with color of this item
+
+      # May fix legend drawing problem at small sizes
+      @d.font = @font if @font
+      @d.pointsize = @legend_font_size
+
+      metrics = @d.get_type_metrics(@base_image, @legend_labels.join(''))
+      legend_text_width = metrics.width
+      legend_width = legend_text_width + 
+                    (@legend_labels.length * legend_square_width * 2.7)
+      legend_left = (@raw_columns - legend_width) / 2
+      legend_increment = legend_width / @legend_labels.length.to_f
+
+      current_x_offset = legend_left
+      current_y_offset =  @hide_title ? 
+                          TOP_MARGIN + LEGEND_MARGIN : 
+                          TOP_MARGIN + 
+                          TITLE_MARGIN + @title_caps_height +
+                          LEGEND_MARGIN
+
+      debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+                                                    
+      @legend_labels.each_with_index do |legend_label, index|        
+
+        # Draw label
+        @d.fill = @font_color
+        @d.font = @font if @font
+        @d.pointsize = scale_fontsize(@legend_font_size)
+        @d.stroke('transparent')
+        @d.font_weight = NormalWeight
+        @d.gravity = WestGravity
+        @d = @d.annotate_scaled( @base_image, 
+                          @raw_columns, 1.0,
+                          current_x_offset + (legend_square_width * 1.7), current_y_offset, 
+                          legend_label.to_s, @scale)
+        
+        # Now draw box with color of this dataset
+        @d = @d.stroke('transparent')
+        @d = @d.fill @data[index][DATA_COLOR_INDEX]
+        @d = @d.rectangle(current_x_offset, 
+                          current_y_offset - legend_square_width / 2.0, 
+                          current_x_offset + legend_square_width, 
+                          current_y_offset + legend_square_width / 2.0)
+
+        @d.pointsize = @legend_font_size
+        metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
+        current_string_offset = metrics.width + (legend_square_width * 2.7)
+        current_x_offset += current_string_offset
+      end
+      @color_index = 0
+    end
+
+    def draw_title
+      return if (@hide_title || @title.nil?)
+      
+      @d.fill = @font_color
+      @d.font = @font if @font
+      @d.stroke('transparent')
+      @d.pointsize = scale_fontsize(@title_font_size)
+      @d.font_weight = BoldWeight
+      @d.gravity = NorthGravity
+      @d = @d.annotate_scaled( @base_image, 
+                        @raw_columns, 1.0,
+                        0, TOP_MARGIN, 
+                        @title, @scale)
+    end
+
+    ##
+    # Draws column labels below graph, centered over x_offset
+    #
+    # TODO Allow WestGravity as an option
+    
+    def draw_label(x_offset, index)
+      return if @hide_line_markers
+
+      if !@labels[index].nil? && @labels_seen[index].nil?
+        y_offset = @graph_bottom + LABEL_MARGIN
+
+        @d.fill = @font_color
+        @d.font = @font if @font
+        @d.stroke('transparent')
+        @d.font_weight = NormalWeight
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity = NorthGravity
+        @d = @d.annotate_scaled(@base_image,
+                                1.0, 1.0,
+                                x_offset, y_offset,
+                                @labels[index], @scale)
+        @labels_seen[index] = 1
+        debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
+      end
+    end
+
+    def draw_no_data
+        @d.fill = @font_color
+        @d.font = @font if @font
+        @d.stroke('transparent')
+        @d.font_weight = NormalWeight
+        @d.pointsize = scale_fontsize(80)
+        @d.gravity = CenterGravity
+        @d = @d.annotate_scaled( @base_image, 
+                        @raw_columns, @raw_rows/2.0,
+                        0, 10, 
+                        @no_data_message, @scale)
+    end
+
+    ##
+    # Finds the best background to render based on the provided theme options.
+    #
+    # Creates a @base_image to draw on.
+    #
+    def render_background
+      case @theme_options[:background_colors]
+      when Array
+        @base_image = render_gradiated_background(*@theme_options[:background_colors])
+      when String
+        @base_image = render_solid_background(@theme_options[:background_colors])
+      else
+        @base_image = render_image_background(*@theme_options[:background_image])
+      end
+    end
+
+    ##
+    # Make a new image at the current size with a solid +color+.
+    
+    def render_solid_background(color)
+      Image.new(@columns, @rows) {
+        self.background_color = color
+      }
+    end
+
+    # Use with a theme definition method to draw a gradiated background.
+    def render_gradiated_background(top_color, bottom_color)
+      Image.new(@columns, @rows, 
+          GradientFill.new(0, 0, 100, 0, top_color, bottom_color))
+    end
+
+    # Use with a theme to use an image (800x600 original) background.
+    def render_image_background(image_path)
+      image = Image.read(image_path)
+      if @scale != 1.0
+        image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph)
+      end
+      image[0]
+    end
+    
+    # Use with a theme to make a transparent background
+    def render_transparent_background
+      Image.new(@columns, @rows) do
+        self.background_color = 'transparent'
+      end
+    end
+
+    def reset_themes
+      @color_index = 0
+      @labels_seen = {}
+      @theme_options = {}
+      
+      @d = Draw.new
+      # Scale down from 800x600 used to calculate drawing.
+      @d = @d.scale(@scale, @scale)
+    end
+
+    def scale(value)
+      value * @scale
+    end
+    
+    # Return a comparable fontsize for the current graph.
+    def scale_fontsize(value)
+      new_fontsize = value * @scale
+      # return new_fontsize < 10.0 ? 10.0 : new_fontsize
+      return new_fontsize
+    end
+
+    def clip_value_if_greater_than(value, max_value)
+      (value > max_value) ? max_value : value
+    end
+
+    # Overridden by subclasses such as stacked bar.
+    def larger_than_max?(data_point, index=0)
+      data_point > @maximum_value
+    end
+
+         def less_than_min?(data_point, index=0)
+      data_point < @minimum_value
+    end
+
+    ##
+    # Overridden by subclasses that need it.
+    def max(data_point, index)
+      data_point
+    end
+
+    ##
+    # Overridden by subclasses that need it.
+         def min(data_point, index)
+      data_point
+    end
+   
+    def significant(inc)
+      return 1.0 if inc == 0 # Keep from going into infinite loop
+      factor = 1.0
+      while (inc < 10)
+        inc *= 10
+        factor /= 10
+      end
+
+      while (inc > 100)
+        inc /= 10
+        factor *= 10
+      end
+
+      res = inc.floor * factor
+      if (res.to_i.to_f == res)
+        res.to_i
+      else
+        res
+      end
+    end
+
+    # Sort with largest overall summed value at front of array 
+    # so it shows up correctly in the drawn graph.
+    def sort_norm_data
+      @norm_data.sort! { |a,b| sums(b[1]) <=> sums(a[1]) }
+    end
+
+    def sums(data_set)
+      total_sum = 0
+      data_set.collect {|num| total_sum += num.to_f }
+      total_sum
+    end
+
+    ##
+    # Used by StackedBar and child classes.
+    #
+    # May need to be moved to the StackedBar class.
+    
+    def get_maximum_by_stack
+      # Get sum of each stack
+      max_hash = {}
+      @data.each do |data_set|
+        data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
+          max_hash[i] = 0.0 unless max_hash[i]
+          max_hash[i] += data_point.to_f
+        end
+      end
+
+      # @maximum_value = 0
+      max_hash.keys.each do |key|
+        @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
+      end
+      @minimum_value = 0
+    end
+
+    def make_stacked
+      stacked_values = Array.new(@column_count, 0)
+      @data.each do |value_set|
+        value_set[1].each_with_index do |value, index|
+          stacked_values[index] += value
+        end
+        value_set[1] = stacked_values.dup
+      end
+    end
+
+private
+    
+    # Takes a block and draws it if DEBUG is true.
+    #
+    #   debug { @d.rectangle x1, y1, x2, y2 }
+    #
+    def debug
+      if DEBUG
+        @d = @d.fill 'transparent'
+        @d = @d.stroke 'turquoise'
+        @d = yield
+      end
+    end
+    
+    def increment_color
+      if @color_index == 0
+        @color_index += 1
+        return @colors[0]
+      else
+        if @color_index < @colors.length
+          @color_index += 1
+          return @colors[@color_index - 1]
+        else
+          # Start over
+          @color_index = 0
+          return @colors[-1]
+        end
+      end
+    end
+
+    ##
+    # Return a formatted string representing a number value that should be printed as a label.   
+
+    def label(value)      
+      if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
+        return value.to_i.to_s
+      end
+      
+      if @spread > 10.0
+        sprintf("%0i", value)
+      elsif @spread >= 3.0
+        sprintf("%0.2f", value)
+      else
+        value.to_s
+      end
+    end
+
+    ##
+    # Returns the height of the capital letter 'X' for the current font and size.
+    #
+    # Not scaled since it deals with dimensions that the regular 
+    # scaling will handle.
+    #
+    def calculate_caps_height(font_size)
+      @d.pointsize = font_size
+      @d.get_type_metrics(@base_image, 'X').height
+    end
+
+    ##
+    # Returns the width of a string at this pointsize.
+    #
+    # Not scaled since it deals with dimensions that the regular 
+    # scaling will handle.
+    #    
+    def calculate_width(font_size, text)
+      @d.pointsize = font_size
+      @d.get_type_metrics(@base_image, text.to_s).width
+    end
+
+  end # Gruff::Base
+  
+  class IncorrectNumberOfDatasetsException < StandardError; end
+          
+end # Gruff
+
+
+module Magick
+  
+  class Draw
+    
+    # Additional method since Draw.scale doesn't affect annotations.
+    def annotate_scaled(img, width, height, x, y, text, scale)
+      scaled_width = (width * scale) >= 1 ? (width * scale) : 1
+      scaled_height = (height * scale) >= 1 ? (height * scale) : 1
+      
+      self.annotate( img, 
+                      scaled_width, scaled_height,
+                      x * scale, y * scale,
+                      text)
+    end
+    
+  end
+  
+end # Magick
+
diff --git a/vendor/plugins/gruff/lib/gruff/deprecated.rb b/vendor/plugins/gruff/lib/gruff/deprecated.rb
new file mode 100644 (file)
index 0000000..be254b5
--- /dev/null
@@ -0,0 +1,39 @@
+
+##
+# A mixin for methods that need to be deleted or have been
+# replaced by cleaner code.
+
+module Gruff
+  module Deprecated
+  
+    def scale_measurements
+      setup_graph_measurements
+    end
+    
+    def total_height
+      @rows + 10
+    end
+    
+    def graph_top
+      @graph_top * @scale
+    end
+    
+    def graph_height
+      @graph_height * @scale
+    end
+    
+    def graph_left 
+      @graph_left * @scale 
+    end
+    
+    def graph_width
+      @graph_width * @scale
+    end
+    
+    # TODO Should be calculate_graph_height
+    # def setup_graph_height
+    #   @graph_height = @graph_bottom - @graph_top
+    # end
+  
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/line.rb b/vendor/plugins/gruff/lib/gruff/line.rb
new file mode 100644 (file)
index 0000000..72416b2
--- /dev/null
@@ -0,0 +1,94 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Line < Gruff::Base
+
+  # Draw a dashed line at the given value
+  attr_accessor :baseline_value
+       
+  # Color of the baseline
+  attr_accessor :baseline_color
+  
+  # Hide parts of the graph to fit more datapoints, or for a different appearance.
+  attr_accessor :hide_dots, :hide_lines
+
+  # Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
+  #
+  #  g = Gruff::Line.new(400) # 400px wide with lines
+  #
+  #  g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
+  #
+  #  g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
+  # 
+  # The preferred way is to call hide_dots or hide_lines instead.
+  def initialize(*args)
+    raise ArgumentError, "Wrong number of arguments" if args.length > 2
+    if args.empty? or ((not Numeric === args.first) && (not String === args.first)) then
+      super()
+    else
+      super args.shift
+    end
+    
+    @hide_dots = @hide_lines = false
+    @baseline_color = 'red'
+    @baseline_value = nil
+  end
+
+  def draw
+    super
+
+    return unless @has_data
+    
+    # Check to see if more than one datapoint was given. NaN can result otherwise.  
+    @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
+     
+    if (defined?(@norm_baseline)) then
+      level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+      @d = @d.push
+      @d.stroke_color @baseline_color
+      @d.fill_opacity 0.0
+      @d.stroke_dasharray(10, 20)
+      @d.stroke_width 5
+      @d.line(@graph_left, level, @graph_left + @graph_width, level)
+      @d = @d.pop
+    end
+
+    @norm_data.each do |data_row|      
+      prev_x = prev_y = nil
+
+      data_row[1].each_with_index do |data_point, index|
+        new_x = @graph_left + (@x_increment * index)
+        next if data_point.nil?
+
+        draw_label(new_x, index)
+
+        new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+        # Reset each time to avoid thin-line errors
+        @d = @d.stroke data_row[DATA_COLOR_INDEX]
+        @d = @d.fill data_row[DATA_COLOR_INDEX]
+        @d = @d.stroke_opacity 1.0
+        @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+
+        if !@hide_lines and !prev_x.nil? and !prev_y.nil? then          
+          @d = @d.line(prev_x, prev_y, new_x, new_y)
+        end
+        circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+        @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
+
+        prev_x = new_x
+        prev_y = new_y
+      end
+
+    end
+
+    @d.draw(@base_image)
+  end
+
+  def normalize
+    @maximum_value = [@maximum_value.to_f, @baseline_value.to_f].max
+    super
+    @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
+  end
+  
+end
diff --git a/vendor/plugins/gruff/lib/gruff/mini/bar.rb b/vendor/plugins/gruff/lib/gruff/mini/bar.rb
new file mode 100644 (file)
index 0000000..6410d68
--- /dev/null
@@ -0,0 +1,32 @@
+##
+#
+# Makes a small bar graph suitable for display at 200px or even smaller.
+#
+module Gruff
+  module Mini
+
+    class Bar < Gruff::Bar
+
+      include Gruff::Mini::Legend
+      
+      def draw
+        @hide_legend = true
+        @hide_title = true
+        @hide_line_numbers = true
+
+        @marker_font_size = 50.0
+        @minimum_value = 0.0
+        @legend_font_size = 60.0
+
+        expand_canvas_for_vertical_legend
+
+        super
+
+        draw_vertical_legend
+        @d.draw(@base_image)
+      end
+
+    end
+  
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/mini/legend.rb b/vendor/plugins/gruff/lib/gruff/mini/legend.rb
new file mode 100644 (file)
index 0000000..25039cf
--- /dev/null
@@ -0,0 +1,77 @@
+module Gruff
+  module Mini
+    module Legend
+      
+      ##
+      # The canvas needs to be bigger so we can put the legend beneath it.
+
+      def expand_canvas_for_vertical_legend
+        @original_rows = @raw_rows
+        @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
+        render_background
+      end
+      
+      ##
+      # Draw the legend beneath the existing graph.
+
+      def draw_vertical_legend
+                
+        @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
+        
+        legend_square_width = 40.0 # small square with color of this item
+        legend_square_margin = 10.0
+        @legend_left_margin = 40.0
+        legend_top_margin = 40.0
+
+        # May fix legend drawing problem at small sizes
+        @d.font = @font if @font
+        @d.pointsize = @legend_font_size
+
+        current_x_offset = @graph_left + @legend_left_margin
+        current_y_offset = @original_rows + legend_top_margin
+
+        debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+
+        @legend_labels.each_with_index do |legend_label, index|        
+
+          # Draw label
+          @d.fill = @font_color
+          @d.font = @font if @font
+          @d.pointsize = scale_fontsize(@legend_font_size)
+          @d.stroke = 'transparent'
+          @d.font_weight = Magick::NormalWeight
+          @d.gravity = Magick::WestGravity
+          @d = @d.annotate_scaled( @base_image, 
+                            @raw_columns, 1.0,
+                            current_x_offset + (legend_square_width * 1.7), current_y_offset, 
+                            truncate_legend_label(legend_label), @scale)
+
+          # Now draw box with color of this dataset
+          @d = @d.stroke 'transparent'
+          @d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX]
+          @d = @d.rectangle(current_x_offset, 
+                            current_y_offset - legend_square_width / 2.0, 
+                            current_x_offset + legend_square_width, 
+                            current_y_offset + legend_square_width / 2.0)
+          
+          current_y_offset += calculate_caps_height(@legend_font_size) * 1.7
+        end
+        @color_index = 0
+      end
+
+      ##
+      # Shorten long labels so they will fit on the canvas.
+      #
+      #   Department of Hu...
+      
+      def truncate_legend_label(label)
+        truncated_label = label.to_s
+        while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - Gruff::Base::RIGHT_MARGIN) && (truncated_label.length > 1)
+          truncated_label = truncated_label[0..truncated_label.length-2]
+        end
+        truncated_label + (truncated_label.length < label.to_s.length ? "…" : '')
+      end
+      
+    end
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/mini/pie.rb b/vendor/plugins/gruff/lib/gruff/mini/pie.rb
new file mode 100644 (file)
index 0000000..7822ba5
--- /dev/null
@@ -0,0 +1,36 @@
+##
+#
+# Makes a small pie graph suitable for display at 200px or even smaller.
+#
+module Gruff
+  module Mini
+
+    class Pie < Gruff::Pie
+
+      include Gruff::Mini::Legend
+
+      def initialize_ivars
+        super
+        
+        @hide_legend = true
+        @hide_title = true
+        @hide_line_numbers = true
+  
+        @marker_font_size = 60.0
+        @legend_font_size = 60.0
+      end
+
+      def draw
+        expand_canvas_for_vertical_legend
+        
+        super
+        
+        draw_vertical_legend
+        
+        @d.draw(@base_image)
+      end # def draw
+
+    end # class Pie
+  
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/mini/side_bar.rb b/vendor/plugins/gruff/lib/gruff/mini/side_bar.rb
new file mode 100644 (file)
index 0000000..9774a50
--- /dev/null
@@ -0,0 +1,22 @@
+##
+#
+# Makes a small pie graph suitable for display at 200px or even smaller.
+#
+module Gruff
+  module Mini
+
+    class SideBar < Gruff::SideBar
+
+      def initialize_ivars
+        super
+        @hide_legend = true
+        @hide_title = true
+        @hide_line_numbers = true
+
+        @marker_font_size = 50.0
+      end
+      
+    end
+  
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/net.rb b/vendor/plugins/gruff/lib/gruff/net.rb
new file mode 100644 (file)
index 0000000..debf9f7
--- /dev/null
@@ -0,0 +1,142 @@
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Spider graph.
+class Gruff::Net < Gruff::Base
+
+  # Hide parts of the graph to fit more datapoints, or for a different appearance.
+  attr_accessor :hide_dots
+
+  def initialize(*args)
+    super
+    
+    @hide_dots = false
+  end
+
+  def draw
+
+    super
+
+    return unless @has_data
+
+    @radius = @graph_height / 2.0
+    @center_x = @graph_left + (@graph_width / 2.0)
+    @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+    @x_increment = @graph_width / (@column_count - 1).to_f
+    circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+
+    @d = @d.stroke_opacity 1.0
+    @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+
+    if (defined?(@norm_baseline)) then
+      level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+      @d = @d.push
+      @d.stroke_color @baseline_color
+      @d.fill_opacity 0.0
+      @d.stroke_dasharray(10, 20)
+      @d.stroke_width 5
+      @d.line(@graph_left, level, @graph_left + @graph_width, level)
+      @d = @d.pop
+    end
+
+    @norm_data.each do |data_row|
+      prev_x = prev_y = nil
+      @d = @d.stroke data_row[DATA_COLOR_INDEX]
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, index|
+        next if data_point.nil?
+
+        rad_pos = index * Math::PI * 2 / @column_count
+        point_distance = data_point * @radius
+        start_x = @center_x + Math::sin(rad_pos) * point_distance
+        start_y = @center_y - Math::cos(rad_pos) * point_distance
+
+        next_index = index + 1 < data_row[1].length ? index + 1 : 0
+
+        next_rad_pos = next_index * Math::PI * 2 / @column_count
+        next_point_distance = data_row[1][next_index] * @radius
+        end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance
+        end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance
+
+        @d = @d.line(start_x, start_y, end_x, end_y)
+
+        @d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots
+      end
+
+    end
+
+    @d.draw(@base_image)
+  end
+
+
+  # the lines connecting in the center, with the first line vertical
+  def draw_line_markers
+    return if @hide_line_markers
+
+
+    # have to do this here (AGAIN)... see draw() in this class
+    # because this funtion is called before the @radius, @center_x and @center_y are set
+    @radius = @graph_height / 2.0
+    @center_x = @graph_left + (@graph_width / 2.0)
+    @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+
+    # Draw horizontal line markers and annotate with numbers
+    @d = @d.stroke(@marker_color)
+    @d = @d.stroke_width 1
+
+
+    (0..@column_count-1).each do |index|
+      rad_pos = index * Math::PI * 2 / @column_count
+
+      @d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius)
+
+
+      marker_label = labels[index] ? labels[index].to_s : '000'
+
+      draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label)
+    end
+  end
+
+private
+
+  def draw_label(center_x, center_y, angle, radius, amount)
+    r_offset = 1.1
+    x_offset = center_x # + 15 # The label points need to be tweaked slightly
+    y_offset = center_y # + 0  # This one doesn't though
+    x = x_offset + (radius * r_offset * Math.sin(angle.deg2rad))
+    y = y_offset - (radius * r_offset * Math.cos(angle.deg2rad))
+
+    # Draw label
+    @d.fill = @marker_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(20)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    s = angle.deg2rad / (2*Math::PI)
+    @d.gravity = SouthGravity     if s >= 0.96 or s < 0.04
+    @d.gravity = SouthWestGravity if s >= 0.04 or s < 0.21
+    @d.gravity = WestGravity      if s >= 0.21 or s < 0.29
+    @d.gravity = NorthWestGravity if s >= 0.29 or s < 0.46
+    @d.gravity = NorthGravity     if s >= 0.46 or s < 0.54
+    @d.gravity = NorthEastGravity if s >= 0.54 or s < 0.71
+    @d.gravity = EastGravity      if s >= 0.71 or s < 0.79
+    @d.gravity = SouthEastGravity if s >= 0.79 or s < 0.96
+#     @d.gravity = NorthGravity
+    @d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale)
+  end
+
+end
+
+# # This method is already in Float
+# class Float
+#   # Used for degree => radian conversions
+#   def deg2rad
+#     self * (Math::PI/180.0)
+#   end
+# end
+
+
+
diff --git a/vendor/plugins/gruff/lib/gruff/photo_bar.rb b/vendor/plugins/gruff/lib/gruff/photo_bar.rb
new file mode 100644 (file)
index 0000000..7240da0
--- /dev/null
@@ -0,0 +1,100 @@
+require File.dirname(__FILE__) + '/base'
+
+# EXPERIMENTAL!
+#
+# Doesn't work yet.
+#
+class Gruff::PhotoBar < Gruff::Base
+
+# TODO
+#
+# define base and cap in yml
+# allow for image directory to be located elsewhere
+# more exact measurements for bar heights (go all the way to the bottom of the graph)
+# option to tile images instead of use a single image
+# drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line
+#
+
+  # The name of a pre-packaged photo-based theme.
+  attr_reader :theme
+
+#   def initialize(target_width=800)
+#     super
+#     init_photo_bar_graphics()
+#   end
+
+  def draw
+    super
+    return unless @has_data
+
+    return # TODO Remove for further development
+
+    init_photo_bar_graphics()
+    
+    #Draw#define_clip_path()
+    #Draw#clip_path(pathname)
+    #Draw#composite....with bar graph image OverCompositeOp
+    #
+    # See also
+    #
+    # Draw.pattern # define an image to tile as the filling of a draw object
+    # 
+
+    # Setup spacing.
+    #
+    # Columns sit side-by-side.
+    spacing_factor = 0.9
+    @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
+
+    @norm_data.each_with_index do |data_row, row_index|
+  
+      data_row[1].each_with_index do |data_point, point_index|
+        data_point = 0 if data_point.nil?
+        # Use incremented x and scaled y
+        left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+        left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
+        right_x = left_x + @bar_width * spacing_factor
+        right_y = @graph_top + @graph_height - 1
+      
+        bar_image_width = data_row[DATA_COLOR_INDEX].columns
+        bar_image_height = right_y.to_f - left_y.to_f
+      
+        # Crop to scale for data
+        bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
+        
+        @d.gravity = NorthWestGravity
+        @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
+      
+        # Calculate center based on bar_width and current row
+        label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
+        draw_label(label_center, point_index)
+      end
+
+    end
+
+    @d.draw(@base_image)    
+  end
+
+
+  # Return the chosen theme or the default
+  def theme
+    @theme || 'plastik'
+  end
+
+protected
+
+  # Sets up colors with a list of images that will be used.
+  # Images should be 340px tall
+  def init_photo_bar_graphics    
+    color_list = Array.new
+    theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
+
+    Dir.open(theme_dir).each do |file|
+      next unless /\.png$/.match(file)
+      color_list << Image.read("#{theme_dir}/#{file}").first
+    end
+    @colors = color_list
+  end
+
+end
+
diff --git a/vendor/plugins/gruff/lib/gruff/pie.rb b/vendor/plugins/gruff/lib/gruff/pie.rb
new file mode 100644 (file)
index 0000000..b143e70
--- /dev/null
@@ -0,0 +1,108 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Pie < Gruff::Base
+
+  TEXT_OFFSET_PERCENTAGE = 0.15
+
+  # Can be used to make the pie start cutting slices at the top (-90.0)
+  # or at another angle. Default is 0.0, which starts at 3 o'clock.
+  attr_accessor :zero_degree
+  
+  def initialize_ivars
+    super
+    @zero_degree = 0.0
+  end
+
+  def draw
+    @hide_line_markers = true
+    
+    super
+
+    return unless @has_data
+
+    diameter = @graph_height
+    radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
+    top_x = @graph_left + (@graph_width - diameter) / 2.0
+    center_x = @graph_left + (@graph_width / 2.0)
+    center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+    total_sum = sums_for_pie()
+    prev_degrees = @zero_degree
+
+    # Use full data since we can easily calculate percentages
+    @data.sort{ |a, b| a[DATA_VALUES_INDEX][0] <=> b[DATA_VALUES_INDEX][0] }.each do |data_row|
+      if data_row[DATA_VALUES_INDEX][0] > 0
+        @d = @d.stroke data_row[DATA_COLOR_INDEX]
+        @d = @d.fill 'transparent'
+        @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
+
+        current_degrees = (data_row[DATA_VALUES_INDEX][0] / total_sum) * 360.0 
+
+        # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
+        # therefore, in order to draw a circle of the proper diameter we must center the stroke at
+        # half the radius for both x and y
+        @d = @d.ellipse(center_x, center_y, 
+                  radius / 2.0, radius / 2.0,
+                  prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
+                  
+        half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
+
+        # End the string with %% to escape the single %.
+        # RMagick must use sprintf with the string and % has special significance.
+        label_string = ((data_row[DATA_VALUES_INDEX][0] / total_sum) * 100.0).round.to_s + '%%'
+        @d = draw_label(center_x,center_y, 
+                    half_angle, radius + (radius * TEXT_OFFSET_PERCENTAGE), 
+                    label_string)
+      
+        prev_degrees += current_degrees
+      end
+    end
+
+    # TODO debug a circle where the text is drawn...
+    
+    @d.draw(@base_image)
+  end
+
+private
+
+  ##
+  # Labels are drawn around a slightly wider ellipse to give room for 
+  # labels on the left and right.
+  def draw_label(center_x, center_y, angle, radius, amount)
+    # TODO Don't use so many hard-coded numbers
+    r_offset = 20.0      # The distance out from the center of the pie to get point
+    x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
+    y_offset = center_y  # This one doesn't though
+    radius_offset = (radius + r_offset)
+    ellipse_factor = radius_offset * 0.15
+    x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle.deg2rad))
+    y = y_offset + (radius_offset * Math.sin(angle.deg2rad))
+    
+    # Draw label
+    @d.fill = @font_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(@marker_font_size)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    @d.gravity = CenterGravity
+    @d.annotate_scaled( @base_image, 
+                      0, 0,
+                      x, y, 
+                      amount, @scale)
+  end
+
+  def sums_for_pie
+    total_sum = 0.0
+    @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX][0] }
+    total_sum
+  end
+
+end
+
+
+class Float
+  # Used for degree => radian conversions
+  def deg2rad
+    self * (Math::PI/180.0)
+  end
+end
diff --git a/vendor/plugins/gruff/lib/gruff/scene.rb b/vendor/plugins/gruff/lib/gruff/scene.rb
new file mode 100644 (file)
index 0000000..b0220eb
--- /dev/null
@@ -0,0 +1,197 @@
+
+require "observer"
+require File.dirname(__FILE__) + '/base'
+
+##
+# A scene is a non-linear graph that assembles layers together to tell a story.
+# Layers are folders with appropriately named files (see below). You can group 
+# layers and control them together or just set their values individually.
+#
+# Examples:
+#
+# * A city scene that changes with the time of day and the weather conditions.
+# * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
+#
+# Usage:
+# 
+#  g = Gruff::Scene.new("500x100", "artwork/city_scene")
+#  g.layers = %w(background haze sky clouds)
+#  g.weather_group = %w(clouds)
+#  g.time_group = %w(background sky)
+#  g.weather = "cloudy"
+#  g.time = Time.now
+#  g.haze = true
+#  g.write "hazy_daytime_city_scene.png"
+#
+#
+#
+# If there is a file named 'default.png', it will be selected (unless other values are provided to override it).
+#
+class Gruff::Scene < Gruff::Base
+    
+  # An array listing the foldernames that will be rendered, from back to front.
+  #
+  #  g.layers = %w(sky clouds buildings street people)
+  #
+  attr_reader :layers
+
+  def initialize(target_width, base_dir)
+    @base_dir = base_dir
+    @groups = {}
+    @layers = []    
+    super target_width
+  end
+
+  def draw
+    # Join all the custom paths and filter out the empty ones
+    image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
+    images = Magick::ImageList.new(*image_paths)
+    @base_image = images.flatten_images
+  end
+
+  def layers=(ordered_list)
+    ordered_list.each do |layer_name|
+      @layers << Gruff::Layer.new(@base_dir, layer_name)
+    end
+  end
+
+  # Group layers to input values
+  #
+  #  g.weather_group = ["sky", "sea", "clouds"]
+  #
+  # Set input values
+  #
+  #  g.weather = "cloudy"
+  #
+  def method_missing(method_name, *args)
+    case method_name.to_s
+    when /^(\w+)_group=$/
+      add_group $1, *args
+      return
+    when /^(\w+)=$/
+      set_input $1, args.first
+      return
+    end
+    super
+  end
+
+private
+
+  def add_group(input_name, layer_names)
+    @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
+  end
+
+  def set_input(input_name, input_value)
+    if not @groups[input_name].nil?
+      @groups[input_name].send_updates(input_value)
+    else
+      if chosen_layer = @layers.detect { |layer| layer.name == input_name }
+        chosen_layer.update input_value
+      end
+    end
+  end
+  
+end
+
+
+class Gruff::Group
+
+  include Observable
+  attr_reader :name
+
+  def initialize(folder_name, layers)
+    @name = folder_name
+    layers.each do |layer|
+      layer.observe self
+    end
+  end
+  
+  def send_updates(value)
+    changed
+    notify_observers value
+  end
+  
+end
+
+
+class Gruff::Layer
+  
+  attr_reader :name
+  
+  def initialize(base_dir, folder_name)
+    @base_dir = base_dir.to_s
+    @name = folder_name.to_s
+    @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
+    @selected_filename = select_default
+  end
+  
+  # Register this layer so it receives updates from the group
+  def observe(obj)
+    obj.add_observer self
+  end
+  
+  # Choose the appropriate filename for this layer, based on the input
+  def update(value)
+    @selected_filename =  case value.to_s
+                          when /^(true|false)$/
+                            select_boolean value
+                          when /^(\w|\s)+$/
+                            select_string value
+                          when /^-?(\d+\.)?\d+$/
+                            select_numeric value
+                          when /(\d\d):(\d\d):\d\d/
+                            select_time "#{$1}#{$2}"
+                          else
+                            select_default
+                          end
+    # Finally, try to use 'default' if we're still blank
+    @selected_filename ||= select_default
+  end
+
+  # Returns the full path to the selected image, or a blank string
+  def path
+    unless @selected_filename.nil? || @selected_filename.empty?
+      return File.join(@base_dir, @name, @selected_filename)
+    end
+    ''
+  end
+
+private
+
+  # Match "true.png" or "false.png"
+  def select_boolean(value)
+    file_exists_or_blank value.to_s
+  end
+
+  # Match -5 to _5.png
+  def select_numeric(value)
+    file_exists_or_blank value.to_s.gsub('-', '_')
+  end
+  
+  def select_time(value)
+    times = @filenames.map { |filename| filename.gsub('.png', '') }
+    times.each_with_index do |time, index|
+      if (time > value) && (index > 0)
+        return "#{times[index - 1]}.png"
+      end
+    end
+    return "#{times.last}.png"
+  end
+  
+  # Match "partly cloudy" to "partly_cloudy.png"
+  def select_string(value)
+    file_exists_or_blank value.to_s.gsub(' ', '_')
+  end
+  
+  def select_default
+    @filenames.include?("default.png") ? "default.png" : ''
+  end
+
+  # Returns the string "#{filename}.png", if it exists.
+  #
+  # Failing that, it returns default.png, or '' if that doesn't exist.
+  def file_exists_or_blank(filename)
+    @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
+  end
+  
+end
diff --git a/vendor/plugins/gruff/lib/gruff/side_bar.rb b/vendor/plugins/gruff/lib/gruff/side_bar.rb
new file mode 100644 (file)
index 0000000..9de3b6a
--- /dev/null
@@ -0,0 +1,117 @@
+require File.dirname(__FILE__) + '/base'
+
+##
+# Graph with individual horizontal bars instead of vertical bars.
+
+class Gruff::SideBar < Gruff::Base
+
+  def draw
+    @has_left_labels = true
+    super
+
+    return unless @has_data
+
+    # Setup spacing.
+    #
+    # Columns sit stacked.
+    spacing_factor = 0.9
+
+    @bar_width = @graph_height / @column_count.to_f
+    @d         = @d.stroke_opacity 0.0
+    height     = Array.new(@column_count, 0)
+    length     = Array.new(@column_count, @graph_left)
+
+    @norm_data.each_with_index do |data_row, row_index|
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, point_index|
+
+         # Using the original calcs from the stacked bar chart 
+         # to get the difference between
+         # part of the bart chart we wish to stack.
+        temp1      = @graph_left + (@graph_width -
+                                data_point * @graph_width - 
+                                height[point_index]) + 1
+        temp2      = @graph_left + @graph_width - height[point_index] - 1
+        difference = temp2 - temp1
+
+        left_x     = length[point_index] #+ 1
+        left_y     = @graph_top + (@bar_width * point_index)
+        right_x    = left_x + difference
+        right_y    = left_y + @bar_width * spacing_factor
+
+        length[point_index] += difference
+        height[point_index] += (data_point * @graph_width - 2)
+
+        @d           = @d.rectangle(left_x, left_y, right_x, right_y)
+
+        # Calculate center based on bar_width and current row
+        label_center = @graph_top + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+        draw_label(label_center, point_index)
+      end
+
+    end
+
+    @d.draw(@base_image)    
+  end
+
+protected
+
+  # Instead of base class version, draws vertical background lines and label
+  def draw_line_markers
+
+    return if @hide_line_markers
+
+    @d = @d.stroke_antialias false
+      
+    # Draw horizontal line markers and annotate with numbers
+    @d = @d.stroke(@marker_color)
+    @d = @d.stroke_width 1
+    number_of_lines = 5
+
+    # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
+    increment = significant(@maximum_value.to_f / number_of_lines)
+    (0..number_of_lines).each do |index|
+
+      line_diff    = (@graph_right - @graph_left) / number_of_lines
+      x            = @graph_right - (line_diff * index) - 1
+      @d           = @d.line(x, @graph_bottom, x, @graph_top)
+      diff         = index - number_of_lines
+      marker_label = diff.abs * increment
+
+      unless @hide_line_numbers
+        @d.fill      = @font_color
+        @d.font      = @font if @font
+        @d.stroke    = 'transparent'
+        @d.pointsize = scale_fontsize(@marker_font_size)
+        @d.gravity   = CenterGravity
+        # TODO Center text over line
+        @d           = @d.annotate_scaled( @base_image, 
+                          0, 0, # Width of box to draw text in
+                          x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
+                          marker_label.to_s, @scale)
+      end # unless
+      @d = @d.stroke_antialias true
+    end
+  end
+
+  ##
+  # Draw on the Y axis instead of the X
+  
+  def draw_label(y_offset, index)
+    if !@labels[index].nil? && @labels_seen[index].nil?
+      @d.fill             = @font_color
+      @d.font             = @font if @font
+      @d.stroke           = 'transparent'
+      @d.font_weight      = NormalWeight
+      @d.pointsize        = scale_fontsize(@marker_font_size)
+      @d.gravity          = EastGravity
+      @d                  = @d.annotate_scaled(@base_image,
+                              1, 1,
+                              -@graph_left + LABEL_MARGIN * 2.0, y_offset,
+                              @labels[index], @scale)
+      @labels_seen[index] = 1
+    end
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb b/vendor/plugins/gruff/lib/gruff/side_stacked_bar.rb
new file mode 100644 (file)
index 0000000..52bbea3
--- /dev/null
@@ -0,0 +1,71 @@
+require File.dirname(__FILE__) + '/base'
+require File.dirname(__FILE__) + '/side_bar'
+
+##
+# New gruff graph type added to enable sideways stacking bar charts 
+# (basically looks like a x/y flip of a standard stacking bar chart)
+#
+# alun.eyre@googlemail.com 
+
+class Gruff::SideStackedBar < Gruff::SideBar
+
+  def draw
+    @has_left_labels = true
+    get_maximum_by_stack
+    super
+
+    return unless @has_data
+
+    # Setup spacing.
+    #
+    # Columns sit stacked.
+    spacing_factor = 0.9
+
+    @bar_width = @graph_height / @column_count.to_f
+    @d = @d.stroke_opacity 0.0
+    height = Array.new(@column_count, 0)
+    length = Array.new(@column_count, @graph_left)
+
+    @norm_data.each_with_index do |data_row, row_index|
+      @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+      data_row[1].each_with_index do |data_point, point_index|
+
+         ## using the original calcs from the stacked bar chart to get the difference between
+         ## part of the bart chart we wish to stack.
+         temp1 = @graph_left + (@graph_width -
+                                    data_point * @graph_width - 
+                                    height[point_index]) + 1
+         temp2 = @graph_left + @graph_width - height[point_index] - 1
+         difference = temp2 - temp1
+
+         left_x = length[point_index] #+ 1
+              left_y = @graph_top + (@bar_width * point_index)
+         right_x = left_x + difference
+              right_y = left_y + @bar_width * spacing_factor
+         length[point_index] += difference
+        height[point_index] += (data_point * @graph_width - 2)
+
+        @d = @d.rectangle(left_x, left_y, right_x, right_y)
+
+        # Calculate center based on bar_width and current row
+        label_center = @graph_top + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+        draw_label(label_center, point_index)
+      end
+
+    end
+
+    @d.draw(@base_image)    
+  end
+
+  protected
+
+  def larger_than_max?(data_point, index=0)
+    max(data_point, index) > @maximum_value
+  end
+
+  def max(data_point, index)
+    @data.inject(0) {|sum, item| sum + item[1][index]}
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/spider.rb b/vendor/plugins/gruff/lib/gruff/spider.rb
new file mode 100644 (file)
index 0000000..7710426
--- /dev/null
@@ -0,0 +1,130 @@
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Net graph.
+#
+# Submitted by Kevin Clark http://glu.ttono.us/
+class Gruff::Spider < Gruff::Base
+  
+  # Hide all text
+  attr_reader :hide_text
+  attr_accessor :hide_axes
+  attr_reader :transparent_background
+  
+  def transparent_background=(value)
+    @transparent_background = value
+    @base_image = render_transparent_background if value
+  end
+
+  def hide_text=(value)
+    @hide_title = @hide_text = value
+  end
+  
+  def initialize(max_value, target_width = 800)
+    super(target_width)
+    @max_value = max_value
+    @hide_legend = true;
+  end
+  
+  def draw
+    @hide_line_markers = true
+    
+    super
+
+    return unless @has_data
+
+    # Setup basic positioning
+    diameter = @graph_height
+    radius = @graph_height / 2.0
+    top_x = @graph_left + (@graph_width - diameter) / 2.0
+    center_x = @graph_left + (@graph_width / 2.0)
+    center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
+    
+    @unit_length = radius / @max_value
+    
+        
+    total_sum = sums_for_spider
+    prev_degrees = 0.0
+    additive_angle = (2 * Math::PI)/ @data.size
+    
+    current_angle = 0.0
+
+    # Draw axes
+    draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes    
+    
+    # Draw polygon
+    draw_polygon(center_x, center_y, additive_angle)
+    
+     
+    @d.draw(@base_image)
+  end
+
+private
+  
+  def normalize_points(value)
+    value * @unit_length
+  end
+  
+  def draw_label(center_x, center_y, angle, radius, amount)
+    r_offset = 50      # The distance out from the center of the pie to get point
+    x_offset = center_x      # The label points need to be tweaked slightly
+    y_offset = center_y + 0  # This one doesn't though
+    x = x_offset + ((radius + r_offset) * Math.cos(angle))
+    y = y_offset + ((radius + r_offset) * Math.sin(angle))
+    
+    # Draw label
+    @d.fill = @marker_color
+    @d.font = @font if @font
+    @d.pointsize = scale_fontsize(legend_font_size)
+    @d.stroke = 'transparent'
+    @d.font_weight = BoldWeight
+    @d.gravity = CenterGravity
+    @d.annotate_scaled( @base_image, 
+                      0, 0,
+                      x, y, 
+                      amount, @scale)
+  end
+  
+  def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
+    return if hide_axes
+    
+    current_angle = 0.0
+    
+    @data.each do |data_row|
+      @d.stroke(line_color || data_row[DATA_COLOR_INDEX])
+      @d.stroke_width 5.0
+    
+      x_offset = radius * Math.cos(current_angle)
+      y_offset = radius * Math.sin(current_angle)
+
+      @d.line(center_x, center_y,
+              center_x + x_offset,
+              center_y + y_offset)
+            
+      draw_label(center_x, center_y, current_angle, radius, data_row[0].to_s) unless hide_text
+            
+      current_angle += additive_angle
+    end
+  end
+  
+  def draw_polygon(center_x, center_y, additive_angle, color = nil)
+    points = []
+    current_angle = 0.0
+    @data.each do |data_row|
+      points << center_x + normalize_points(data_row[1][0]) * Math.cos(current_angle)
+      points << center_y + normalize_points(data_row[1][0]) * Math.sin(current_angle)
+      current_angle += additive_angle
+    end
+    
+    @d.stroke_width 1.0
+    @d.stroke(color || @marker_color)
+    @d.fill(color || @marker_color)
+    @d.fill_opacity 0.4
+    @d.polygon(*points)
+  end
+  
+  def sums_for_spider
+    @data.inject(0.0) {|sum, data_row| sum += data_row[1][0]}
+  end
+
+end
diff --git a/vendor/plugins/gruff/lib/gruff/stacked_bar.rb b/vendor/plugins/gruff/lib/gruff/stacked_bar.rb
new file mode 100644 (file)
index 0000000..bf28df7
--- /dev/null
@@ -0,0 +1,49 @@
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::StackedBar < Gruff::Base
+
+    # Draws a bar graph, but multiple sets are stacked on top of each other.
+    def draw
+      get_maximum_by_stack
+      super
+      return unless @has_data
+
+      # Setup spacing.
+      #
+      # Columns sit stacked.
+      spacing_factor = 0.9
+      @bar_width = @graph_width / @column_count.to_f
+    
+      @d = @d.stroke_opacity 0.0
+      
+      height = Array.new(@column_count, 0)
+    
+      @norm_data.each_with_index do |data_row, row_index|
+        @d = @d.fill data_row[DATA_COLOR_INDEX]
+      
+        data_row[1].each_with_index do |data_point, point_index|
+          # Use incremented x and scaled y
+          left_x = @graph_left + (@bar_width * point_index)
+          left_y = @graph_top + (@graph_height -
+                                 data_point * @graph_height - 
+                                 height[point_index]) + 1
+          right_x = left_x + @bar_width * spacing_factor
+          right_y = @graph_top + @graph_height - height[point_index] - 1
+          
+          # update the total height of the current stacked bar
+          height[point_index] += (data_point * @graph_height - 2)
+          
+          @d = @d.rectangle(left_x, left_y, right_x, right_y)
+          
+          # Calculate center based on bar_width and current row
+          label_center = @graph_left + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+          draw_label(label_center, point_index)
+        end
+
+      end
+    
+      @d.draw(@base_image)    
+    end
+
+end

Benjamin Mako Hill || Want to submit a patch?