You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ode.apache.org by as...@apache.org on 2007/11/20 23:33:54 UTC

svn commit: r596855 - in /ode/sandbox/singleshot: ./ app/controllers/ app/models/ config/initializers/ db/migrate/ lib/ lib/extensions/ spec/models/ vendor/plugins/ vendor/plugins/annotate_models/ vendor/plugins/annotate_models/lib/ vendor/plugins/anno...

Author: assaf
Date: Tue Nov 20 14:33:52 2007
New Revision: 596855

URL: http://svn.apache.org/viewvc?rev=596855&view=rev
Log:
Added preliminary Task/Stakeholder models, included annotate_models, removed BCrypt (licensing)

Added:
    ode/sandbox/singleshot/NOTICE
    ode/sandbox/singleshot/app/models/stakeholder.rb
    ode/sandbox/singleshot/db/migrate/002_create_tasks.rb
    ode/sandbox/singleshot/db/migrate/003_create_stakeholders.rb
    ode/sandbox/singleshot/lib/extensions/
    ode/sandbox/singleshot/lib/extensions.rb
    ode/sandbox/singleshot/lib/extensions/enumerable.rb
    ode/sandbox/singleshot/spec/models/
    ode/sandbox/singleshot/spec/models/task_spec.rb
    ode/sandbox/singleshot/vendor/plugins/annotate_models/
    ode/sandbox/singleshot/vendor/plugins/annotate_models/ChangeLog
    ode/sandbox/singleshot/vendor/plugins/annotate_models/README
    ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/
    ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/annotate_models.rb
    ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/
    ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/annotate_models_tasks.rake
Modified:
    ode/sandbox/singleshot/README
    ode/sandbox/singleshot/app/controllers/application.rb
    ode/sandbox/singleshot/app/models/person.rb
    ode/sandbox/singleshot/app/models/task.rb
    ode/sandbox/singleshot/config/initializers/libs.rb
    ode/sandbox/singleshot/vendor/plugins/   (props changed)

Added: ode/sandbox/singleshot/NOTICE
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/NOTICE?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/NOTICE (added)
+++ ode/sandbox/singleshot/NOTICE Tue Nov 20 14:33:52 2007
@@ -0,0 +1,7 @@
+Rails, copyright of David Heinemeier Hansson, released under the MIT license.
+
+Annotate Models, copyright of Dave Thomas, Pragmatic Programmers, LLC, released under the Ruby license.
+
+Exception Notifier, copyright of Jamis Buck, released under the MIT license
+
+RSpec, copyright of The RSpec Development Team, released under the MIT license.

Modified: ode/sandbox/singleshot/README
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/README?rev=596855&r1=596854&r2=596855&view=diff
==============================================================================
--- ode/sandbox/singleshot/README (original)
+++ ode/sandbox/singleshot/README Tue Nov 20 14:33:52 2007
@@ -6,7 +6,6 @@
 
   $ cd singleshot
   $ rake rails:freeze:edge
-  $ sudo gem install bcrypt-ruby
 
 
 == Setting up the database

Modified: ode/sandbox/singleshot/app/controllers/application.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/application.rb?rev=596855&r1=596854&r2=596855&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/controllers/application.rb (original)
+++ ode/sandbox/singleshot/app/controllers/application.rb Tue Nov 20 14:33:52 2007
@@ -4,88 +4,79 @@
 class ApplicationController < ActionController::Base
 
   helper :all # include all helpers, all the time
+
   # Turn sessions off for JSON, XML, Atom, etc.  We only use sessions for login/flash in HTML pages
   # and XHR requests.
   session :if=>lambda { |req| !(req.format.html? || req.xhr?) }
 
-  # Methods dealing with authentication.
-  module Authentication
-  
-  protected
-
-    # Returns Person object for currently authenticated user.
-    attr_reader :authenticated
-
-    def authenticate
-      if request.format.html?
-        authenticate_using_session
-      else
-        authenticate_using_http_basic
-      end
-    end
 
-    # Use HTTP Basic authentication.
-    def authenticate_using_http_basic
-      authenticate_or_request_with_http_basic do |login, password|
-        @authenticated = Person.authenticate(login, password)
-      end
-    end
+  # --- Authentication ---
 
-    # Use session-based authentication.  If not currently authenticated in session,
-    # redirects to session_url.  Set person_id on login, reset on logout.
-    def authenticate_using_session
-      @authenticated = Person.find_by_id(session[:person_id])
-      unless @authenticated
-        flash[:follow] = request.url
-        redirect_to(session_url)
-      end
-    end
+  # Returns Person object for currently authenticated user.
+  attr_reader :authenticated
 
-    # Authenticate using access_key parameter for cases where we cannot use sessions,
-    # HTTP Basic or any other mechanism, for example, for accessing feeds and iCalendar.
-    # This authentication only works for GET requests.
-    def authenticate_using_access_key
-      @authenticated = Person.find_by_access_key(params[:access_key]) if request.get?
-      head :not_found unless @authenticated
+  def authenticate
+    if request.format.html?
+      authenticate_using_session
+    else
+      authenticate_using_http_basic
     end
+  end
 
-    # Returns query string parameters for authentication, see #authenticate_using_access_key.
-    def access_key_authentication_parameters
-      { :access_key=>authenticated.access_key }
+  # Use HTTP Basic authentication.
+  def authenticate_using_http_basic
+    authenticate_or_request_with_http_basic do |login, password|
+      @authenticated = Person.authenticate(login, password)
     end
+  end
 
+  # Use session-based authentication.  If not currently authenticated in session,
+  # redirects to session_url.  Set person_id on login, reset on logout.
+  def authenticate_using_session
+    @authenticated = Person.find_by_id(session[:person_id])
+    unless @authenticated
+      flash[:follow] = request.url
+      redirect_to(session_url)
+    end
   end
 
+  # Authenticate using access_key parameter for cases where we cannot use sessions,
+  # HTTP Basic or any other mechanism, for example, for accessing feeds and iCalendar.
+  # This authentication only works for GET requests.
+  def authenticate_using_access_key
+    @authenticated = Person.find_by_access_key(params[:access_key]) if request.get?
+    head :not_found unless @authenticated
+  end
 
-  # Methods dealing with the authenticated user.
-  module Authenticated
+  # Returns query string parameters for authentication, see #authenticate_using_access_key.
+  def access_key_authentication_parameters
+    { :access_key=>authenticated.access_key }
+  end
 
-  protected
 
-    # Returns language code for currently authenticated user (may be nil).
-    def language
-      authenticated.language
-    end
+  # --- Authenticated user ---
 
-    # Given the time (defaults to now) returns an adjusted time based on the authenticated user's timezone.
-    # This should be used when presenting formatted time without the timezone.
-    def tz_adjust(time = Time.now)
-      return time unless authenticated.timezone
-      timezone = TimeZone[authenticated.timezone]
-      timezone ? timezone.adjust(time) : time
-    end
+  # Returns language code for currently authenticated user (may be nil).
+  def language
+    authenticated.language
+  end
 
-    # Given the time (Time or string) returns unadjusted time (to UTC) based on the authenticated user's timezone.
-    # This should be used for time inputs provided without the timezone.
-    def tz_unadjust(time)
-      time = Time.parse(time.to_s) unless Time === time
-      return time unless authenticated.timezone
-      timezone = TimeZone[authenticated.timezone]
-      timezone ? timezone.unadjust(time) : time
-    end
+  # Given the time (defaults to now) returns an adjusted time based on the authenticated user's timezone.
+  # This should be used when presenting formatted time without the timezone.
+  def tz_adjust(time = Time.now)
+    return time unless authenticated.timezone
+    timezone = TimeZone[authenticated.timezone]
+    timezone ? timezone.adjust(time) : time
+  end
 
+  # Given the time (Time or string) returns unadjusted time (to UTC) based on the authenticated user's timezone.
+  # This should be used for time inputs provided without the timezone.
+  def tz_unadjust(time)
+    time = Time.parse(time.to_s) unless Time === time
+    return time unless authenticated.timezone
+    timezone = TimeZone[authenticated.timezone]
+    timezone ? timezone.unadjust(time) : time
   end
 
-  include Authentication, Authenticated
 
 end

Modified: ode/sandbox/singleshot/app/models/person.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/person.rb?rev=596855&r1=596854&r2=596855&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/models/person.rb (original)
+++ ode/sandbox/singleshot/app/models/person.rb Tue Nov 20 14:33:52 2007
@@ -1,5 +1,5 @@
 # == Schema Information
-# Schema version: 1
+# Schema version: 3
 #
 # Table name: people
 #
@@ -10,12 +10,13 @@
 #  language   :string(5)     
 #  timezone   :integer(4)    
 #  password   :string(64)    
+#  access_key :string(32)    default(""), not null
+#  site_url   :string(255)   
 #  created_at :datetime      
 #  updated_at :datetime      
 #
 
-require 'md5'
-require 'bcrypt'
+require 'sha1'
 
 # Internally we keep a primary key association between the person and various other records.
 # Externally, we use a public identifier returned from #to_param and resolved with Person.identify.
@@ -84,15 +85,18 @@
   # TODO:  Some way to check minimum size of passwords.
 
   def password=(password)
-    write_attribute :password, BCrypt::Password.create(password )
+    seed = SHA1.hexdigest(OpenSSL::Random.random_bytes(128))[0,10]
+    crypted = SHA1.hexdigest("#{seed}:#{password}")
+    self[:password] = "#{seed}:#{crypted}"
   end
 
   def password?(password)
-    BCrypt::Password.new(read_attribute(:password)) == password
+    seed, crypted = self[:password].split(':')
+    crypted == SHA1.hexdigest("#{seed}:#{password}")
   end
 
   def reset_password!
-    password = Array.new(10).map { (65 + rand(58)).chr }.join
+    password = Array.new(10) { (65 + rand(58)).chr }.join
     self.password = password  
     save!
     password
@@ -101,7 +105,7 @@
   attr_protected :access_key
 
   def access_key!
-    self.access_key = MD5.hexdigest(OpenSSL::Random.random_bytes(128))
+    self.access_key = SHA1.hexdigest(OpenSSL::Random.random_bytes(128))
     save! unless new_record?
   end
 

Added: ode/sandbox/singleshot/app/models/stakeholder.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/stakeholder.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/models/stakeholder.rb (added)
+++ ode/sandbox/singleshot/app/models/stakeholder.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,52 @@
+# == Schema Information
+# Schema version: 3
+#
+# Table name: stakeholders
+#
+#  id         :integer(11)   not null, primary key
+#  task_id    :integer(11)   not null
+#  person_id  :integer(11)   not null
+#  role       :integer(2)    not null
+#  created_at :datetime      
+#  updated_at :datetime      
+#
+
+# Represents a stakeholder in the task.  Identifies the person and their role.
+# Some roles allow multiple people, others do not.  This distinction is handled by
+# the Task itself.
+class Stakeholder < ActiveRecord::Base
+
+  module Roles
+
+    # Roles for stakeholders associated with a task.  Supports the following roles:
+    # * :creator         -- Person who created the task, specified at creation.
+    # * :owner           -- Person who currently owns (performs) the task.
+    # * :potential_owner -- Person who is allowed to claim (become owner of) the task.
+    # * :admin           -- Admins are allowed to modify the task, change its status, etc.
+    #
+    # Role is stored as index value, but presented elsewhere as symbol, so when adding new roles,
+    # make sure to append but not to remove or change the order of roles in this list.
+    ROLES = [:creator, :owner, :potential_owner, :admin]
+
+    # The following roles allow one stakeholder per task: creator, owner.
+    SINGULAR_ROLES = [:creator, :owner]
+
+    # These roles allow multiple stakeholders per task.
+    PLURAL_ROLES = ROLES - SINGULAR_ROLES
+
+  end
+
+  include Roles
+
+  # Stakeholder associated with a task.
+  belongs_to :task
+
+  # Stakeholder associated with a person.
+  belongs_to :person
+  validates_presence_of :person_id
+
+  # Enumerated role, see Task::Roles for more details.
+  enumerable :role, ROLES
+  validates_presence_of :role
+
+end

Modified: ode/sandbox/singleshot/app/models/task.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/task.rb?rev=596855&r1=596854&r2=596855&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/models/task.rb (original)
+++ ode/sandbox/singleshot/app/models/task.rb Tue Nov 20 14:33:52 2007
@@ -1,16 +1,12 @@
 # == Schema Information
-# Schema version: 1
+# Schema version: 3
 #
 # Table name: tasks
 #
 #  id                :integer(11)   not null, primary key
-#  name              :string(255)   default(""), not null
-#  title             :string(4000)  default(""), not null
-#  priority          :integer(11)   
-#  task_url          :string(2000)  
+#  title             :string(255)   default(""), not null
 #  status            :integer(2)    default(0), not null
-#  complete_at       :datetime      
-#  completion_url    :string(2000)  
+#  completion_url    :string(255)   
 #  completion_format :string(255)   
 #  access_key        :string(32)    
 #  data              :text          default(""), not null
@@ -19,7 +15,163 @@
 #  updated_at        :datetime      
 #
 
+require 'openssl'
+require 'md5'
+
 class Task < ActiveRecord::Base
-  
+
+  # Locking column used for versioning and detecting update conflicts.
+  set_locking_column 'version'
+
+  def initialize(*args)
+    super
+    self.status = :active
+    self.access_key = MD5.hexdigest(OpenSSL::Random.random_bytes(128))
+  end
+
+  def to_param
+    "#{id}-#{title.gsub(/[^\w]+/, '-').gsub(/-*$/, '')}"
+  end
+
+
+  # --- Task state ---
+
+  enumerable :status, [:active, :suspended, :completed, :cancelled], :check_methods=>true
+  validates_presence_of :status
+  attr_protected :status
+
+
+  # --- Completion and cancellation ---
+
+  # Supported formats for sending completion notification.
+  COMPLETION_FORMATS = [Mime::JSON, Mime::XML]
+
+  #normalize_urls :completion_url
+  #validates_http_url :completion_url,
+  #  :message=>"All tasks must have a valid completion URL (HTTPS or HTTP)"
+  validates_inclusion_of :completion_format, :in=>COMPLETION_FORMATS,
+    :message=>"Supported completion formats are #{COMPLETION_FORMATS.to_sentence}"
+
+  # Returns the completion format as a Mime type.
+  def completion_format
+    Mime::EXTENSION_LOOKUP[self[:completion_format]] || Mime::XML
+  end
+
+  # Sets the completion format using a Mime type.
+  def completion_format=(content_type)
+    self[:completion_format] = content_type.to_sym.to_s
+  end
+
+
+  # --- Task data ---
+
+  def data
+    @data ||= (ActiveSupport::JSON.decode(self[:data] || '') || {}).with_indifferent_access
+  end
+
+  def data=(data)
+    raise ArgumentError, 'Must be a hash' unless Hash === data
+    @data = data
+  end
+
+  before_save do |record|
+    record.instance_eval do
+      self[:data], @data = @data.to_json, nil if @data
+    end 
+  end
+
+
+  # --- Stakeholders ---
+
+  include Stakeholder::Roles
+
+  # Stakeholders and people (as stakeholders) associated with this task.
+  has_many :stakeholders, :include=>:person, :dependent=>:delete_all
+  has_many :people, :through=>:stakeholders
+ 
+  # :call-seq:
+  #    in_role(*roles) => Person*
+  #    in_role(:any) => Person*
+  #
+  # Returns a list of all stakeholders (Person objects) associated with a given role (symbol).
+  # You can pass an array of roles, or use :any to return all stakeholders.
+  def in_role(*roles)
+    roles = roles.flatten
+    if roles.include?(:any)
+      stakeholders.map { |sh| sh.person }
+    else
+      stakeholders.select { |sh| roles.include?(sh.role) }.map { |sh| sh.person }
+    end
+  end
+
+  # :call-seq:
+  #   in_role?(identity, *roles) => boolean
+  #
+  # Returns true if given person is associated with this task in a particular role.
+  # You can also query against a set of roles, or any roles by passing :any.
+  def in_role?(identity, *roles)
+    in_role(roles).include?(Person.identify(identity))
+  end
+
+  # Adds methods for singular roles, like this:
+  # * owner?(identity) -- Returns true if person is in this role
+  # * owner            -- Returns person in this role
+  # * owner = identity -- Assigns a person to this role
+  SINGULAR_ROLES.each do |role|
+    define_method("#{role}?") { |identity| in_role?(Person.identify(identity), role) }
+    define_method(role) { in_role(role).first }
+    define_method "#{role}=" do |identity|
+      current = stakeholders.detect { |sh| sh.role == role }
+      if identity
+        person = Person.identify(identity)
+        if current.nil? || person != current.person
+          stakeholders.delete current if current
+          stakeholders.build :person=>person, :role=>role
+        end
+      elsif current
+        stakeholders.delete current
+      end
+    end
+
+    validate do |record|
+      record.errors.add role, 'A task can have only one #{role}' if record.in_role(role).size > 1
+    end
+  end
+
+  # Adds methods for plural roles, like this:
+  # * admin?(identity)    -- Returns true if person is in this role
+  # * admins              -- Returns all people in this role
+  # * admins = identities -- Assigns person/people to this role
+  PLURAL_ROLES.each do |role|
+    plural = role.to_s.pluralize
+    define_method("#{role}?") { |identity| in_role?(Person.identify(identity), role) }
+    define_method(plural) { in_role(role) }
+    define_method "#{plural}=" do |identities|
+      new_set = Array(identities).map { |identity| Person.identify(identity) }.uniq
+      existing = stakeholders.select { |sh| sh.role == role }
+      stakeholders.delete existing.reject { |sh| new_set.include?(sh.person) }
+      (new_set - existing.map { |sh| sh.person }).each { |person| stakeholders.build :person=>person, :role=>role }
+    end
+  end
+
+
+  # --- Access control ---
+
+  attr_protected :access_key
+
+  # Returns a token allowing that particular person to access the task.
+  # The token is validated by calling #authorize.  The token is only valid
+  # if the person is a stakeholder in the task, and based on their role.
+  def token_for(person)
+    MD5.hexdigest("#{person.to_param}:#{access_key}")
+  end
+
+  # Returns the person authorized to access this task based on the token returned
+  # by #token_for.  The person is guaranteed to be a stakeholder in the task.
+  # Returns nil if the token is invalid or the person is no longer associated with
+  # this task.
+  def authorize(token)
+    stakeholders.map { |sh| sh.person }.uniq.find { |person| token_for(person) == token }
+  end
 
 end

Modified: ode/sandbox/singleshot/config/initializers/libs.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/initializers/libs.rb?rev=596855&r1=596854&r2=596855&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/initializers/libs.rb (original)
+++ ode/sandbox/singleshot/config/initializers/libs.rb Tue Nov 20 14:33:52 2007
@@ -1 +1,2 @@
 require 'open-uri'
+require File.join(RAILS_ROOT, 'lib/extensions')

Added: ode/sandbox/singleshot/db/migrate/002_create_tasks.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/db/migrate/002_create_tasks.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/db/migrate/002_create_tasks.rb (added)
+++ ode/sandbox/singleshot/db/migrate/002_create_tasks.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,19 @@
+class CreateTasks < ActiveRecord::Migration
+  def self.up
+    create_table :tasks do |t|
+      t.string    :title,             :null=>false
+      t.integer   :status,            :null=>false, :default=>0, :limit=>2
+      t.string    :completion_url,    :null=>true
+      t.string    :completion_format, :null=>true
+      t.string    :access_key,        :null=>true, :limit=>32
+      t.text      :data,              :null=>false
+      t.integer   :version,           :null=>false, :default=>0
+      t.timestamps
+    end
+    add_index :tasks, [:status, :updated_at]
+  end
+
+  def self.down
+    drop_table :tasks
+  end
+end

Added: ode/sandbox/singleshot/db/migrate/003_create_stakeholders.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/db/migrate/003_create_stakeholders.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/db/migrate/003_create_stakeholders.rb (added)
+++ ode/sandbox/singleshot/db/migrate/003_create_stakeholders.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,17 @@
+class CreateStakeholders < ActiveRecord::Migration
+  def self.up
+    create_table :stakeholders do |t|
+      t.integer :task_id,    :null=>false
+      t.integer :person_id,  :null=>false
+      t.integer :role,       :null=>false, :limit=>2
+      t.timestamps
+    end
+    add_index :stakeholders, [:task_id, :person_id, :role], :unique=>true
+    add_index :stakeholders, [:task_id, :role]
+    add_index :stakeholders, [:person_id, :role]
+  end
+
+  def self.down
+    drop_table :stakeholders
+  end
+end

Added: ode/sandbox/singleshot/lib/extensions.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/lib/extensions.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/lib/extensions.rb (added)
+++ ode/sandbox/singleshot/lib/extensions.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1 @@
+Dir[File.join(File.dirname(__FILE__), 'extensions/*.rb')].each { |file| require file }

Added: ode/sandbox/singleshot/lib/extensions/enumerable.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/lib/extensions/enumerable.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/lib/extensions/enumerable.rb (added)
+++ ode/sandbox/singleshot/lib/extensions/enumerable.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,74 @@
+module ActiveRecord
+  class Base
+    class << self
+
+      # Handles the named attribute as enumeration using the specified symbols.
+      # For example:
+      #   enumerable :state, [:open, :closed, :cancelled], :check_methods=>true
+      # Allows:
+      #   record.state = :open
+      #   record.state
+      #   => :open
+      #   record.state_before_typecast
+      #   => 0
+      #   record.open?
+      #   => true
+      #   Record::STATES
+      #   => [:open, :closed, :cancelled]
+      #   Record.state(:closed)
+      #   => 1
+      # 
+      # Allowed options:
+      # * :constant -- Specifies the constant that will hold the enumerated symbols,
+      #   or false to not create a constant.  The default behavior uses the pluralized
+      #   name of the attribute.
+      # * :default  -- Specifies a default value to apply to the attribute, otherwise,
+      #   attributes with no specified value return nil.
+      # * :check_methods -- If true, adds a check method for each enumerated value,
+      #   for example, open? to return true if status == :open.
+      # * :validates -- Unless false, adds validates_inclusion_of rule.
+      def enumerable(attr_name, *args)
+        options = args.extract_options!
+        symbols = args.flatten
+        # Define constant, if not already defined.
+        case options[:constant]
+        when false
+        when nil
+          const_name = attr_name.to_s.pluralize.upcase
+          const_get(const_name) rescue const_set const_name, symbols
+        else
+          const_set options[:constant].to_s.upcase, symbols
+        end
+        # Read/write methods.
+        define_method attr_name do
+          if value = read_attribute(attr_name)
+            symbols[value]
+          elsif value = options[:default]
+            write_attribute attr_name, symbols.index(value)
+            value
+          end
+        end
+        define_method "#{attr_name}=" do |value|
+          write_attribute attr_name, symbols.index(value || options[:default])
+        end
+        # Class method to convert symbol into index.
+        class << self ; self ; end.instance_eval do
+          define_method(attr_name) { |symbol| symbols.index(symbol) }
+        end
+        # Validation.
+        unless options[:validate] == false
+          condition = options[:condition] || lambda { |record| record.send(attr_name) }
+          validates_inclusion_of attr_name, :in=>symbols, :if=>condition,
+            :message=>options[:message] || "Allowed values for attribute #{attr_name} are #{symbols.to_sentence}"
+        end
+        # Check methods (e.g. foo?, bar?).
+        if options[:check_methods]
+          symbols.each do |symbol|
+            define_method("#{symbol}?") { send(attr_name) == symbol }
+          end
+        end
+      end
+
+    end
+  end
+end

Added: ode/sandbox/singleshot/spec/models/task_spec.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/spec/models/task_spec.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/spec/models/task_spec.rb (added)
+++ ode/sandbox/singleshot/spec/models/task_spec.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,388 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+module TaskSpec
+
+  def default_values
+    { :title=>'Test Task model' }
+  end
+
+  def person(role)
+    Person.find_by_nickname(role.to_s) || Person.create(:email=>"#{role}@apache.org")
+  end
+
+end
+
+
+describe Task, 'to_param' do
+  include TaskSpec
+
+  before :each do
+    @task = Task.create(default_values)
+  end
+
+  def title
+    @task.to_param[/(\d+-)(.*)/, 2]
+  end
+
+  it 'should resolve to task ID' do
+    @task.to_param.to_i.should == @task.id 
+  end
+
+  it 'should contain title' do
+    title.should == @task.title.gsub(' ', '-')
+  end
+
+  it 'should change all non-word characters to dashes' do
+    @task.title = 'foo*bar+baz faz_'
+    title.should == 'foo-bar-baz-faz_'
+  end
+
+  it 'should consolidate multiple dashes' do
+    @task.title = 'foo**bar--baz -faz'
+    title.should == 'foo-bar-baz-faz'
+  end
+
+  it 'should remove dashes at end' do
+    @task.title = 'foo**'
+    title.should == 'foo'
+  end
+
+end
+
+
+describe Task, 'status' do
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values)
+  end
+
+  it 'should default to :active' do
+    @task.status.should == :active
+  end
+
+  it 'should allow valid status code' do
+    Task::STATUSES.each do |status|
+      @task.status = status
+      @task.save
+      Task.find(@task.id).status.should == status
+    end
+  end
+
+  it 'should reject unknown status code' do
+    @task.status = :unknown
+    lambda { @task.save! }.should raise_error(ActiveRecord::RecordInvalid)
+  end
+
+  it 'should require status code' do
+    @task[:status] = nil
+    lambda { @task.save! }.should raise_error(ActiveRecord::RecordInvalid)
+  end
+
+  it 'should protect status code from mass assignment' do
+    lambda { @task.update_attributes :status=>:completed }.should_not change { @task.status }
+  end
+
+  it 'should resolve symbol to index value' do
+    Task::STATUSES.each_with_index do |status, i|
+      Task.status(status).should be(i)
+    end
+  end
+
+end
+
+
+describe Task, 'completion format' do
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values.except(:completion_format))
+  end
+
+  it 'should default to Mime::XML' do
+    @task.completion_format.should == Mime::XML
+  end
+
+  it 'should accept Mime::XML' do
+    @task.update_attributes :completion_format=>Mime::XML
+    Task.find(@task.id).completion_format.should == Mime::XML
+  end
+
+  it 'should accept Mime::JSON' do
+    @task.update_attributes :completion_format=>Mime::JSON
+    Task.find(@task.id).completion_format.should == Mime::JSON
+  end
+
+  it 'should reject unsupported formats' do
+    @task.completion_format = Mime::ATOM
+    lambda { @task.save! }.should raise_error(ActiveRecord::RecordInvalid)
+  end
+
+end
+
+
+describe Task, 'data' do
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values)
+  end
+
+  it 'should return hash' do
+    @task.data.should be_kind_of(Hash)
+  end
+
+  it 'should accept hash' do
+    @task.data = { }
+  end
+
+  it 'should be hash with indifferent access' do
+    @task.update_attributes :data=>{ :foo=>1 }
+    Task.find(@task.id).data[:foo].should be(1)
+    Task.find(@task.id).data['foo'].should be(1)
+  end
+
+  it 'should reject any other value' do
+    lambda { @task.data = nil }.should raise_error(ArgumentError)
+    lambda { @task.data = [] }.should raise_error(ArgumentError)
+    lambda { @task.data = 'string' }.should raise_error(ArgumentError)
+  end
+
+  it 'should store changes' do
+    lambda { @task.update_attributes :data=>{:foo=>1} }.should change { @task.id && Task.find(@task.id).data }.to('foo'=>1)
+    lambda do
+      @task.data[:bar] = 2
+      @task.save
+    end.should change { @task.id && Task.find(@task.id).data.to_hash }.to('foo'=>1, 'bar'=>2)
+  end
+
+end
+
+
+describe Task, 'token' do
+
+  include TaskSpec
+
+  before :all do
+    @creator = person('creator')
+    @owner = person('owner')
+    @task = Task.new(:creator=>@creator, :owner=>@owner)
+    @creator_token = @task.token_for(@creator)
+    @owner_token = @task.token_for(@owner)
+  end
+
+  it 'should be MD5 digest' do
+    @creator_token.should match(/^[a-z0-9]{32}$/)
+    @owner_token.should match(/^[a-z0-9]{32}$/)
+  end
+
+  it 'should be consistent for a given person' do
+    @creator_token.should == @task.token_for(@creator)
+    @owner_token.should == @task.token_for(@owner)
+  end
+
+  it 'should be different for different people' do
+    @creator_token.should_not == @owner_token
+  end
+
+  it 'should resolve back to person' do
+    @task.authorize(@creator_token).should be(@creator)
+    @task.authorize(@owner_token).should be(@owner)
+  end
+
+end
+
+
+describe Task, 'singular role' do
+
+  include TaskSpec
+
+  before :all do
+    @person = person('person')
+  end
+
+  before :each do
+    @task = Task.new(default_values)
+  end
+
+  it 'should start as nil' do
+    Task::SINGULAR_ROLES.each do |role|
+      @task.send(role).should be_nil
+    end
+  end
+
+  it 'should have getter and setter methods' do
+    Task::SINGULAR_ROLES.each do |role|
+      lambda { @task.update_attributes role=>@person }.should change(@task, role).to(@person)
+    end
+  end
+
+  it 'should have checker methods' do
+    Task::SINGULAR_ROLES.each do |role|
+      lambda { @task.update_attributes role=>@person }.should change { @task.send("#{role}?", @person) }.to(true)
+    end
+  end
+
+  it 'should be able to remove person' do
+    Task::SINGULAR_ROLES.each do |role|
+      @task.update_attributes role=>@person
+      lambda { @task.update_attributes role=>nil }.should change(@task, role).from(@person).to(nil)
+    end
+  end
+
+  it 'should accept person at creation' do
+    Task::SINGULAR_ROLES.each do |role|
+      Task.create(default_values.merge(role=>person(role))).send(role).should eql(person(role))
+    end
+  end
+
+end
+
+
+describe Task, 'plural role' do
+
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values)
+  end
+
+  def plural(role)
+    role.to_s.pluralize
+  end
+
+  def people
+    @people ||= Array.new(3) { |i| person("person#{i}") }
+  end
+
+  it 'should start as empty array' do
+    Task::PLURAL_ROLES.each do |role|
+      @task.send(plural(role)).should be_empty
+    end
+  end
+
+  it 'should have getter and setter methods' do
+    Task::PLURAL_ROLES.each do |role|
+      lambda { @task.update_attributes plural(role)=>people }.should change(@task, plural(role)).to(people)
+    end
+  end
+
+  it 'should have checker methods' do
+    Task::PLURAL_ROLES.each do |role|
+      lambda { @task.update_attributes plural(role)=>people.first }.should change { @task.send("#{role}?", people.first) }.to(true)
+      @task.send("#{role}?", people.last).should be_false
+    end
+  end
+
+  it 'should be able to add person' do
+    Task::PLURAL_ROLES.each do |role|
+      @task.update_attributes plural(role)=>people[0..-2]
+      lambda { @task.update_attributes plural(role)=>people }.should change(@task, plural(role)).
+        from(people[0..-2]).to(people)
+    end
+  end
+
+  it 'should be able to remove person' do
+    Task::PLURAL_ROLES.each do |role|
+      @task.update_attributes plural(role)=>people
+      lambda { @task.update_attributes plural(role)=>people[0..-2] }.should change(@task, plural(role)).
+        from(people).to(people[0..-2])
+    end
+  end
+
+  it 'should add each person only once' do
+    Task::PLURAL_ROLES.each do |role|
+      @task.update_attributes plural(role)=>[people.first, people.first]
+      @task.send(plural(role)).should eql([people.first])
+    end
+  end
+
+  it 'should accept person at creation' do
+    Task::PLURAL_ROLES.each do |role|
+      Task.create(default_values.merge(plural(role)=>people)).send(plural(role)).should eql(people)
+    end
+  end
+
+end
+
+
+describe Task, 'creator' do
+
+  include TaskSpec
+
+  it 'should be singular role' do
+    Task::SINGULAR_ROLES.should include(:creator)
+  end
+
+  it 'should default to no one' do
+    Task.new.creator.should be_nil
+  end
+
+end
+
+
+describe Task, 'owner' do
+
+  it 'should be singular role' do
+    Task::SINGULAR_ROLES.should include(:owner)
+  end
+
+  it 'should default to no one' do
+    Task.new.owner.should be_nil
+  end
+  
+end
+
+
+describe Task, 'in_role' do
+
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values.merge(:owner=>person(:owner), :creator=>person(:creator)))
+  end
+
+  it 'should return all people in that role' do
+    @task.in_role(:owner).should eql([person('owner')])
+  end
+
+  it 'should return all people in multiple roles' do
+    @task.in_role(:owner, :creator).sort_by(&:id).should eql([person('owner'), person('creator')].sort_by(&:id))
+  end
+
+  it 'should return all people for :any' do
+    @task.in_role(:any).sort_by(&:id).should eql([person('owner'), person('creator')].sort_by(&:id))
+  end
+
+end
+
+
+describe Task, 'in_role?' do
+
+  include TaskSpec
+
+  before :each do
+    @task = Task.new(default_values.merge(:owner=>person(:owner), :creator=>person(:creator)))
+  end
+
+  it 'should return true if person in that role' do
+    @task.in_role?(person(:owner), :owner).should be_true
+    @task.in_role?(person(:creator), :creator).should be_true
+  end
+
+  it 'should return true if person is in any of the roles' do
+    @task.in_role?(person(:owner), :creator, :owner).should be_true
+    @task.in_role?(person(:creator), :creator, :owner).should be_true
+  end
+
+  it 'should return false if person not in role' do
+    @task.in_role?(person(:owner), :creator).should be_false
+    @task.in_role?(person(:creator), :owner).should be_false
+  end
+
+  it 'should return true if person in any role for :any' do
+    @task.in_role?(person(:owner), :any).should be_true
+    @task.in_role?(person(:unknown), :any).should be_false
+  end
+
+end

Propchange: ode/sandbox/singleshot/vendor/plugins/
------------------------------------------------------------------------------
--- svn:externals (original)
+++ svn:externals Tue Nov 20 14:33:52 2007
@@ -1,4 +1,2 @@
-exception_notification        http://dev.rubyonrails.com/svn/rails/plugins/exception_notification
-annotate_models               http://repo.pragprog.com/svn/Public/plugins/annotate_models
 rspec                         svn://rubyforge.org/var/svn/rspec/trunk/rspec
 rspec_on_rails                svn://rubyforge.org/var/svn/rspec/trunk/rspec_on_rails

Added: ode/sandbox/singleshot/vendor/plugins/annotate_models/ChangeLog
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/vendor/plugins/annotate_models/ChangeLog?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/vendor/plugins/annotate_models/ChangeLog (added)
+++ ode/sandbox/singleshot/vendor/plugins/annotate_models/ChangeLog Tue Nov 20 14:33:52 2007
@@ -0,0 +1,46 @@
+2007-03-05: Dave Thomas <da...@pragprog.com>
+   * Forgot to call the quote method
+   
+2007-03-02: Dave Thomas <da...@pragprog.com>
+   * Allow non-printing characters in column defaults (suggested by Ben Booth)
+
+2007-02-28: Dave Thomas <da...@pragprog.com>
+   * Report errors loading model classes better. Change suggested by Niels Knacke
+   
+2007-02-22: Dave Thomas <da...@pragprog.com>
+* Ignore models with no underlying database table (based on patch from Jamie van Dyke)
+* Handle case where database has no session_info table (patch by David Vrensk)
+   
+   
+2006-07-13: Dave Thomas <da...@pragprog.com>
+   * Support :scale for decimal columns
+   
+2006-07-13: Wes Gamble
+  * Don't annotate abstract models
+  
+2006-06-13: Dave Thomas <da...@pragprog.com>
+  * Fix bug where we corrupted the PREFIX string and therefore duplicated
+    the header
+  * No longer include the datetime, so we don't trigger a commit
+    back into repos
+    
+    -- NOTE -- just this once, you'll get a duplicate header after you run
+    a_m on an already-annotated model. Sorry.... Dave
+    
+    
+    
+2006-06-11  Dave Thomas  <da...@pragprog.com>
+  * lib/annotate_models.rb: At Kian Wright's suggestion, document the table
+    name and primary key. Also make the timestamp prettier
+    
+2006-04-17  Dave Thomas  <da...@pragprog.com>
+
+	* lib/annnotate_models.rb: Include Bruce William's patch to allow
+		  models in subdirectories
+
+2006-03-11  Dave Thomas  <da...@pragprog.com>
+
+	* lib/annotate_models.rb: Use camelize, not classify, to construct
+	  class names (Grant Hollingworth)
+
+3/3/06   Now annotates fixture files too (thanks to Josha Susser)

Added: ode/sandbox/singleshot/vendor/plugins/annotate_models/README
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/vendor/plugins/annotate_models/README?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/vendor/plugins/annotate_models/README (added)
+++ ode/sandbox/singleshot/vendor/plugins/annotate_models/README Tue Nov 20 14:33:52 2007
@@ -0,0 +1,31 @@
+AnnotateSchema
+==============
+
+Add a comment summarizing the current schema to the top
+of each ActiveRecord model source file:
+
+  # Schema as of Sun Feb 26 21:58:32 CST 2006 (schema version 7)
+  #
+  #  id                  :integer(11)   not null
+  #  quantity            :integer(11)   
+  #  product_id          :integer(11)   
+  #  unit_price          :float         
+  #  order_id            :integer(11)   
+  #
+
+  class LineItem < ActiveRecord::Base belongs_to :product
+  
+   . . .
+  
+Note that this code will blow away the initial comment block in your models if it looks ike it was
+previously added by annotate models, so you don't want to add additional text to an automatically
+created comment block.
+
+Author:
+   Dave Thomas
+   Pragmatic Programmers, LLC
+   
+Released under the same license as Ruby. No Support. No Warranty.
+
+Back up your model files before using...
+

Added: ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/annotate_models.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/annotate_models.rb?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/annotate_models.rb (added)
+++ ode/sandbox/singleshot/vendor/plugins/annotate_models/lib/annotate_models.rb Tue Nov 20 14:33:52 2007
@@ -0,0 +1,127 @@
+require "config/environment"
+
+MODEL_DIR   = File.join(RAILS_ROOT, "app/models")
+FIXTURE_DIR = File.join(RAILS_ROOT, "test/fixtures")
+
+module AnnotateModels
+
+  PREFIX = "== Schema Information"
+  
+  # Simple quoting for the default column value
+  def self.quote(value)
+    case value
+      when NilClass                 then "NULL"
+      when TrueClass                then "TRUE"
+      when FalseClass               then "FALSE"
+      when Float, Fixnum, Bignum    then value.to_s
+      # BigDecimals need to be output in a non-normalized form and quoted.
+      when BigDecimal               then value.to_s('F')
+      else
+        value.inspect
+    end
+  end
+
+  # Use the column information in an ActiveRecord class
+  # to create a comment block containing a line for
+  # each column. The line contains the column name,
+  # the type (and length), and any optional attributes
+  def self.get_schema_info(klass, header)
+    info = "# #{header}\n#\n"
+    info << "# Table name: #{klass.table_name}\n#\n"
+    
+    max_size = klass.column_names.collect{|name| name.size}.max + 1
+    klass.columns.each do |col|
+      attrs = []
+      attrs << "default(#{quote(col.default)})" if col.default
+      attrs << "not null" unless col.null
+      attrs << "primary key" if col.name == klass.primary_key
+
+      col_type = col.type.to_s
+      if col_type == "decimal"
+        col_type << "(#{col.precision}, #{col.scale})"
+      else
+        col_type << "(#{col.limit})" if col.limit
+      end 
+      info << sprintf("#  %-#{max_size}.#{max_size}s:%-13.13s %s\n", col.name, col_type, attrs.join(", "))
+    end
+
+    info << "#\n\n"
+  end
+
+  # Add a schema block to a file. If the file already contains
+  # a schema info block (a comment starting
+  # with "Schema as of ..."), remove it first.
+
+  def self.annotate_one_file(file_name, info_block)
+    if File.exist?(file_name)
+      content = File.read(file_name)
+
+      # Remove old schema info
+      content.sub!(/^# #{PREFIX}.*?\n(#.*\n)*\n/, '')
+
+      # Write it back
+      File.open(file_name, "w") { |f| f.puts info_block + content }
+    end
+  end
+  
+  # Given the name of an ActiveRecord class, create a schema
+  # info block (basically a comment containing information
+  # on the columns and their types) and put it at the front
+  # of the model and fixture source files.
+
+  def self.annotate(klass, header)
+    info = get_schema_info(klass, header)
+    
+    model_file_name = File.join(MODEL_DIR, klass.name.underscore + ".rb")
+    annotate_one_file(model_file_name, info)
+
+    fixture_file_name = File.join(FIXTURE_DIR, klass.table_name + ".yml")
+    annotate_one_file(fixture_file_name, info)
+  end
+
+  # Return a list of the model files to annotate. If we have 
+  # command line arguments, they're assumed to be either
+  # the underscore or CamelCase versions of model names.
+  # Otherwise we take all the model files in the 
+  # app/models directory.
+  def self.get_model_names
+    models = ARGV.dup
+    models.shift
+    
+    if models.empty?
+      Dir.chdir(MODEL_DIR) do 
+        models = Dir["**/*.rb"]
+      end
+    end
+    models
+  end
+
+  # We're passed a name of things that might be 
+  # ActiveRecord models. If we can find the class, and
+  # if its a subclass of ActiveRecord::Base,
+  # then pas it to the associated block
+
+  def self.do_annotations
+    header = PREFIX.dup
+    version = ActiveRecord::Migrator.current_version rescue 0
+    if version > 0
+      header << "\n# Schema version: #{version}"
+    end
+    
+    self.get_model_names.each do |m|
+      class_name = m.sub(/\.rb$/,'').camelize
+      begin
+        klass = class_name.split('::').inject(Object){ |klass,part| klass.const_get(part) }
+        if klass < ActiveRecord::Base && !klass.abstract_class?
+          puts "Annotating #{class_name}"
+          self.annotate(klass, header)
+        else
+          puts "Skipping #{class_name}"
+        end
+      rescue Exception => e
+        puts "Unable to annotate #{class_name}: #{e.message}"
+      end
+      
+    end
+  end
+end

Added: ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/annotate_models_tasks.rake
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/annotate_models_tasks.rake?rev=596855&view=auto
==============================================================================
--- ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/annotate_models_tasks.rake (added)
+++ ode/sandbox/singleshot/vendor/plugins/annotate_models/tasks/annotate_models_tasks.rake Tue Nov 20 14:33:52 2007
@@ -0,0 +1,6 @@
+desc "Add schema information (as comments) to model files"
+
+task :annotate_models do
+   require File.join(File.dirname(__FILE__), "../lib/annotate_models.rb")
+   AnnotateModels.do_annotations
+end
\ No newline at end of file