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