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/12/12 00:14:43 UTC

svn commit: r603406 [1/3] - in /ode/sandbox/singleshot: ./ app/controllers/ app/helpers/ app/models/ app/views/layouts/ app/views/sandwiches/ app/views/sessions/ app/views/tasks/ config/ config/environments/ config/initializers/ db/migrate/ doc/pages/ ...

Author: assaf
Date: Tue Dec 11 15:14:39 2007
New Revision: 603406

URL: http://svn.apache.org/viewvc?rev=603406&view=rev
Log:
Added more code around tasks and dummy task implementation (sandwich)

Added:
    ode/sandbox/singleshot/app/controllers/sandwiches_controller.rb
    ode/sandbox/singleshot/app/models/sandwich.rb
    ode/sandbox/singleshot/app/views/layouts/head.html.erb
    ode/sandbox/singleshot/app/views/sandwiches/
    ode/sandbox/singleshot/app/views/sandwiches/show.html.erb
    ode/sandbox/singleshot/app/views/tasks/
    ode/sandbox/singleshot/app/views/tasks/new.html.erb
    ode/sandbox/singleshot/app/views/tasks/show.html.erb
    ode/sandbox/singleshot/config/lighttpd.conf
    ode/sandbox/singleshot/lib/extensions/instance.rb
    ode/sandbox/singleshot/lib/patches/
    ode/sandbox/singleshot/lib/patches.rb
    ode/sandbox/singleshot/lib/patches/client_error.rb
    ode/sandbox/singleshot/lib/patches/http_basic.rb
    ode/sandbox/singleshot/lib/patches/to_xml_primitive.rb
    ode/sandbox/singleshot/lib/singleshot/
    ode/sandbox/singleshot/lib/singleshot.rb
    ode/sandbox/singleshot/lib/singleshot/resource.rb
    ode/sandbox/singleshot/lib/singleshot/task.rb
    ode/sandbox/singleshot/public/stylesheets/buttons.css   (with props)
    ode/sandbox/singleshot/public/stylesheets/print.css   (with props)
    ode/sandbox/singleshot/public/stylesheets/screen.css   (with props)
Removed:
    ode/sandbox/singleshot/lib/extensions/to_xml_primitive.rb
Modified:
    ode/sandbox/singleshot/NOTICE
    ode/sandbox/singleshot/app/controllers/application.rb
    ode/sandbox/singleshot/app/controllers/tasks_controller.rb
    ode/sandbox/singleshot/app/helpers/application_helper.rb
    ode/sandbox/singleshot/app/models/person.rb
    ode/sandbox/singleshot/app/models/stakeholder.rb
    ode/sandbox/singleshot/app/models/task.rb
    ode/sandbox/singleshot/app/views/layouts/application.html.erb
    ode/sandbox/singleshot/app/views/sessions/show.html.erb
    ode/sandbox/singleshot/config/environment.rb
    ode/sandbox/singleshot/config/environments/development.rb
    ode/sandbox/singleshot/config/initializers/libs.rb
    ode/sandbox/singleshot/config/routes.rb
    ode/sandbox/singleshot/db/migrate/001_create_people.rb
    ode/sandbox/singleshot/db/migrate/002_create_tasks.rb
    ode/sandbox/singleshot/doc/pages/managing_tasks.textile
    ode/sandbox/singleshot/lib/extensions.rb
    ode/sandbox/singleshot/lib/extensions/enumerable.rb
    ode/sandbox/singleshot/lib/extensions/json_request.rb
    ode/sandbox/singleshot/lib/extensions/validators.rb
    ode/sandbox/singleshot/public/javascripts/application.js
    ode/sandbox/singleshot/public/javascripts/prototype.js
    ode/sandbox/singleshot/public/stylesheets/default.css
    ode/sandbox/singleshot/spec/common.rb
    ode/sandbox/singleshot/spec/controllers/tasks_controller_spec.rb
    ode/sandbox/singleshot/spec/models/task_spec.rb
    ode/sandbox/singleshot/spec/views/sessions/show_spec.rb

Modified: ode/sandbox/singleshot/NOTICE
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/NOTICE?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/NOTICE (original)
+++ ode/sandbox/singleshot/NOTICE Tue Dec 11 15:14:39 2007
@@ -1,13 +1,86 @@
-This software includes code and material from the following sources:
+This software uses Rails, copyright of David Heinemeier Hansson.
 
-Rails, copyright of David Heinemeier Hansson, distributed under the MIT license.
+Copyright (c) 2004-2007 David Heinemeier Hansson
 
-Prototype, copyright of Sam Stephenson, distributed under the MIT license.
+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:
 
-script.aculo.us, copyright of Thomas Fuchs, Ivan Krstic, Jon Tirsen and Sammi Williams, distributed under the MIT license.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
 
-Annotate Models, copyright of Dave Thomas, Pragmatic Programmers, LLC, distributed under the Ruby license.
+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.
 
-Exception Notifier, copyright of Jamis Buck, distributed under the MIT license
 
-RSpec, copyright of The RSpec Development Team, distributed under the MIT license.
+Includes the prototype.js library, copyright of Sam Stephenson,
+distributed under the MIT license.
+
+
+Includes the script.aculo.us library, copyright of Thomas Fuchs, Ivan Krstic,
+Jon Tirsen and Sammi Williams, distributed under the MIT license.
+
+
+Includes the Annotate Models plugin, copyright of Dave Thomas, Pragmatic
+Programmers, LLC, distributed under the Ruby license.
+
+
+Includes RSpec and RSpec on Rails, copyright of their respective owners.
+
+RSpec, Copyright (c) 2005-2007 The RSpec Development Team
+ARTS, Copyright (c) 2006 Kevin Clark, Jake Howerton
+ZenTest, Copyright (c) 2001-2006 Ryan Davis, Eric Hodel, Zen Spider Software
+AssertSelect, Copyright (c) 2006 Assaf Arkin
+
+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.
+
+
+Theme adapted from Blueprint CSS, copyright of Olav Bjorkoy, buttons plugin
+by Kevin Hale.
+
+Copyright (c) 2007 Olav Bjorkoy (http://bjorkoy.com)
+
+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, sub-license, 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 every other copyright notice found in this 
+software, and all the attributions in every file, 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 NON-INFRINGEMENT. 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.

Modified: ode/sandbox/singleshot/app/controllers/application.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/application.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/controllers/application.rb (original)
+++ ode/sandbox/singleshot/app/controllers/application.rb Tue Dec 11 15:14:39 2007
@@ -6,21 +6,28 @@
   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.
+  # and XHR requests, so only turn them on for these cases.  This also forces HTTP Basic authentication
+  # on all other request types.
   session :off, :if=>lambda { |req| !(req.format.html? || req.xhr?) }
 
 
   # --- Authentication ---
 
+  # Raise to return 403 (Forbidden) with optional error message.
+  class NotAuthorized < Exception
+  end
+  rescue_responses[NotAuthorized.name] = :forbidden
+
   # Returns Person object for currently authenticated user.
   attr_reader :authenticated
-  helper_method :authenticated
 
+  # Use as filter to authenticate using either HTTP Basic Authentication or sessions.
+  # Authenticates using HTTP Basic if authentication header provided, and forces HTTP Basic
+  # if sessions are disabled (e.g. JSON and XML content types).  Otherwise, authenticates
+  # using sessions and stores the authenticated person's identifier in person_id.
   def authenticate
-    # Always allow HTTP Basic Authentication, regardless of the format, but
-    # use HTTP Basic in lieu of sessions (i.e. JSON or XML request formats).
     if ActionController::HttpAuthentication::Basic.authorization(request) || !session_enabled?
-      authenticate_or_request_with_http_basic 'Singleshot' do |login, password|
+      authenticate_or_request_with_http_basic request.domain do |login, password|
         @authenticated = Person.authenticate(login, password)
       end
     else
@@ -55,6 +62,9 @@
 
   # 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.
+  #
+  # For example:
+  #   tz_adjust(item.updated_at).to_s
   def tz_adjust(time = Time.now)
     return time unless authenticated.timezone
     timezone = TimeZone[authenticated.timezone]
@@ -63,12 +73,14 @@
 
   # 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.
+  #
+  # For example:
+  #   due_on = tz_adjust(params[:due_on])
   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
-
 
 end

Added: ode/sandbox/singleshot/app/controllers/sandwiches_controller.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/sandwiches_controller.rb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/controllers/sandwiches_controller.rb (added)
+++ ode/sandbox/singleshot/app/controllers/sandwiches_controller.rb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,45 @@
+class SandwichesController < ApplicationController
+
+  before_filter :instance
+  before_filter :update_instance, :only=>[:update, :create]
+
+  def show
+    @read_only = true unless params['perform'] == 'true'
+  end
+
+  def update
+    if @sandwich.save
+      flash[:success] = 'Changes have been saved.'
+      redirect_to :action=>'show', :task_url=>@task_url, :perform=>true
+    else
+      render :action=>'show'
+    end
+  end
+
+  def create
+    if @sandwich.complete
+      flash[:success] = 'Thank you.  Sandwich created!'
+      redirect_to :action=>'show', :task_url=>@task_url, :perform=>true
+    else
+      render :action=>'show'
+    end
+  end
+
+private
+
+  def instance
+    @task_url = params['task_url'] or raise ActiveRecord::RecordNotFound
+    @sandwich = Sandwich.load(@task_url)
+    @read_only = true if @sandwich.status == 'completed'
+  end
+
+  def update_instance
+    if params = self.params['sandwich']
+      params['toppings'] = params['toppings'] * ';'
+      @sandwich.attributes = params
+    else
+      head :bad_request
+    end 
+  end
+
+end

Modified: ode/sandbox/singleshot/app/controllers/tasks_controller.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/controllers/tasks_controller.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/controllers/tasks_controller.rb (original)
+++ ode/sandbox/singleshot/app/controllers/tasks_controller.rb Tue Dec 11 15:14:39 2007
@@ -1,8 +1,11 @@
 class TasksController < ApplicationController
 
-  before_filter :authenticate
-  verify :params=>:task, :only=>[:create, :update, :complete], :render=>{:text=>'Missing task', :status=>:bad_request}
-  before_filter :find_task, :only=>[:show, :update, :complete, :destroy]
+  verify :params=>:task, :only=>:update, :render=>{:text=>'Missing task', :status=>:bad_request}
+  before_filter :authenticate, :except=>[:show, :update, :complete, :destroy]
+  instance :task, :only=>[:show, :update, :complete, :destroy], :check=>:instance_accessible
+  before_filter :forbid_reserved, :except=>[:update, :destroy]
+
+  layout 'head', :only=>[:show]
 
   def new
     @task = Task.new(:creator=>authenticated)
@@ -10,14 +13,19 @@
   end
 
   def create
-    # Outcome MIME type depends on request content type.
-    outcome_type = Task::OUTCOME_MIME_TYPES.include?(request.content_type) ? request.content_type : Mime::XML
-    admins = (params[:task][:admins] || []) << authenticated
-    @task = Task.create!(params[:task].merge(:outcome_type=>outcome_type, :admins=>admins))
-    respond_to do |format|
-      format.html { redirect_to tasks_url }
-      format.xml  { render :xml=>@task, :location=>task_url(@task), :status=>:created }
-      format.json { render :json=>@task, :location=>task_url(@task), :status=>:created }
+    if input = params[:task]
+      input[:outcome_type] = suggested_outcome_type
+      input[:admins] = Array(input[:admins]) + [authenticated]
+      input.delete(:status)
+      @task = Task.create!(input)
+      respond_to do |format|
+        format.html { redirect_to tasks_url }
+        format.xml  { render :xml=>@task, :location=>task_url(@task), :status=>:created }
+        format.json { render :json=>@task, :location=>task_url(@task), :status=>:created }
+      end
+    else
+      task = Task.reserve!(authenticated)
+      render :nothing=>true, :location=>task_url(task), :status=>:see_other
     end
   end
 
@@ -30,59 +38,66 @@
   end
 
   def update
-    @task.update_attributes! filter_update(params[:task])
+    # TODO: conditional put
+    raise ActiveRecord::StaleObjectError, 'This task already completed.' if @task.completed?
+    input = params[:task]
+    input[:outcome_type] ||= suggested_outcome_type unless @task.outcome_type
+    filter = @task.filter_update_for(authenticated)
+    raise NotAuthorized, 'You are not allowed to change this task.' unless filter
+    input = filter[input]
+    raise NotAuthorized, 'You cannot make this change.' unless input
+
+    @task.update_attributes! input
     respond_to do |format|
-      format.html { redirect_to :back }
+      format.html { redirect_to task_url }
       format.xml  { render :xml=>@task }
       format.json { render :json=>@task }
     end
   end
 
   def complete
-    if @task.complete!(authenticated, params[:task][:data])
-      respond_to do |format|
-        format.html { redirect_to tasks_url }
-        format.xml  { render :xml=>@task }
-        format.json { render :json=>@task }
-      end 
-    else
-      error = 'You are not allowed to complete this task'
-      respond_to do |format|
-        format.html do
-          flash[:error] = error
-          redirect_to :back rescue redirect_to root_url
-        end
-        format.all { render :text=>error, :status=>:forbidden }
-      end 
+    raise ActiveRecord::StaleObjectError, 'This task already completed.' if @task.completed?
+    raise NotAuthorized, 'You are not allowed to complete this task.' unless @task.can_complete?(authenticated)
+    data = params[:task][:data] if params[:task]
+    @task.complete!(data)
+    respond_to do |format|
+      format.xml  { render :xml=>@task }
+      format.json { render :json=>@task }
     end
   end
 
   def destroy
-    if @task.cancel!(authenticated)
-      respond_to do |format|
-        format.html { redirect_to tasks_url }
-        format.all  { head :ok }
-      end 
-    else
-      error = 'You are not allowed to cancel this task'
-      respond_to do |format|
-        format.html do
-          flash[:error] = error
-          redirect_to :back rescue redirect_to root_url
-        end
-        format.all { render :text=>error, :status=>:forbidden }
-      end 
-    end
+    raise ActiveRecord::StaleObjectError, 'This task already completed, you cannot cancel it.' if @task.completed?
+    raise NotAuthorized, 'You are not allowed to cancel this task.' unless @task.can_cancel?(authenticated)
+      @task.cancel!
+    head :ok
   end
 
 private
 
-  def find_task
-    @task = Task.find(params[:id])
-    # Must be stakeholder to access the task, send 404 if the task does not 'exist' for this person.
-    raise ActiveRecord::RecordNotFound unless @task.stakeholder?(authenticated)
-    # Cancelled tasks exist in their own resource.
-    raise ActiveRecord::RecordNotFound if @task.cancelled?
+  # Authenticate and make sure the instance is accessible.  Use this instead of the authentication
+  # filter to allow access control based on token authentication.  Precludes access to cancelled tasks.:w
+
+  def instance_accessible(task)
+    # Use _token authentication (HTTP Basic) to authorize stakeholder associated with task
+    # otherwise use regular authentication.
+    authenticate_with_http_basic do |login, token|
+      @authenticated = instance.authorize(token) if login == '_token'
+    # Task accessible if:
+    # - Authenticated user is stakeholder
+    # - Task has not been cancelled (deleted)
+    end or authenticate
+    task.stakeholder?(authenticated) && !task.cancelled? if authenticated
+  end
+
+  # Return 403 (Forbidden) when accessing a reserved task (show, complete)
+  def forbid_reserved
+    raise ActiveRecord::RecordNotFound if @task && @task.reserved?
+  end
+
+  # Determines the outcome content type based on the request content type.
+  def suggested_outcome_type
+    Task::OUTCOME_MIME_TYPES.include?(request.content_type) ? request.content_type : Mime::XML
   end
 
 end

Modified: ode/sandbox/singleshot/app/helpers/application_helper.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/helpers/application_helper.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/helpers/application_helper.rb (original)
+++ ode/sandbox/singleshot/app/helpers/application_helper.rb Tue Dec 11 15:14:39 2007
@@ -3,14 +3,24 @@
 
   # Returns a link to a person using their full name as the link text and site URL
   # (or profile, if unspecified) as the reference.
-  def link_to_person(person, options = nil)
-    options ||= {} 
-    person.site_url ?
-      link_to(h(person.fullname), person.site_url, options.reverse_merge(:title=>"See #{person.fullname}'s profile")) :
-      content_tag('span', h(person.fullname), options)
+  def link_to_person(person, options = {})
+    fullname = h(person.fullname)
+    fullname = '&lt;<unknown;&gt;' if fullname.blank?
+    person.site_url ? link_to(fullname, person.site_url, options.reverse_merge(:title=>"See #{fullname}'s profile")) :
+      content_tag('span', fullname, options)
   end
 
   # Returns Person object for currently authenticated user.
   attr_reader :authenticated
+
+  def relative_date_with_adjustment(date)
+    date = controller.tz_adjust(date.utc) if Time === date
+    relative_date(date)
+  end
+
+  def relative_date_with_abbr(date)
+    date = controller.tz_adjust(date.utc) if Time === date
+    content_tag 'abbr', h(relative_date(date)), :title=>date.to_date.to_s
+  end
 
 end

Modified: ode/sandbox/singleshot/app/models/person.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/person.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/models/person.rb (original)
+++ ode/sandbox/singleshot/app/models/person.rb Tue Dec 11 15:14:39 2007
@@ -4,7 +4,7 @@
 # Table name: people
 #
 #  id         :integer(11)   not null, primary key
-#  nickname   :string(255)   default(""), not null
+#  identity   :string(255)   default(""), not null
 #  fullname   :string(255)   default(""), not null
 #  email      :string(255)   default(""), not null
 #  language   :string(5)     
@@ -12,6 +12,7 @@
 #  password   :string(64)    
 #  access_key :string(32)    default(""), not null
 #  site_url   :string(255)   
+#  admin      :boolean(1)    not null
 #  created_at :datetime      
 #  updated_at :datetime      
 #
@@ -20,7 +21,7 @@
 
 # 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.
-# The base implementation uses the person's nickname as the public identifier.
+# The base implementation uses the person's nickname (derived from e-mail address) as the public identifier.
 #
 # In addition, we need to know the person's full name for presentation and e-mail address for
 # sending notifications.  Language and timezone (in minutes relative to UTC) are optional,
@@ -44,13 +45,13 @@
         identities.map { |identity| identify(identity) }.uniq 
       else
         identity = identities.first
-        Person === identity ? identity : Person.find_by_nickname(identity)
+        Person === identity ? identity : Person.find_by_identity(identity)
       end
     end
 
     # Used for identity/password authentication.  Return the person if authenticated.
     def authenticate(login, password)
-      person = Person.find_by_nickname(login)
+      person = Person.find_by_identity(login)
       person if person && person.password?(password)
     end
 
@@ -63,28 +64,29 @@
 
   # Returns an identifier suitable for use with Person.resolve.
   def to_param
-    nickname
+    identity
   end
 
-  # Must have nickname.
-  validates_uniqueness_of :nickname, :message=>'A person with this nickname already exists.'
-  before_validation do |record|
-    record.nickname = record.email.to_s.strip[/([^\s@]*)/, 1].downcase if record.nickname.blank?
-    record.nickname = record.nickname.gsub(/\s+/, '').downcase
+  def same_as?(person)
+    person == (person.is_a?(Person) ? self : to_param)
   end
 
-  # Must have fullname.
-  before_validation do |record|
-    record.fullname = record.nickname.to_s.split(/[_.]+/).map(&:capitalize).join(' ') if record.fullname.blank?
-    record.fullname = record.fullname.strip.gsub(/\s+/, ' ')
-  end
+  # Must have identity.
+  validates_uniqueness_of :identity, :message=>'A person with this identity already exists.'
 
   # Must have e-mail address.
   validates_email         :email, :message=>"I need a valid e-mail address."
   validates_uniqueness_of :email, :message=>'This e-mail is already in use.'
-  before_validation do |record|
-    record.email = record.email.to_s.strip.downcase
+
+  before_validation :fix_attributes
+  def fix_attributes
+    self.email = email.to_s.strip.downcase
+    self.identity = email.to_s.strip[/([^\s@]*)/, 1].downcase if identity.blank?
+    self.identity = identity.strip.gsub(/\s+/, '_').downcase
+    self.fullname = email.to_s.strip[/([^\s@]*)/, 1].split(/[_.]+/).map(&:capitalize).join(' ') if fullname.blank?
+    self.fullname = fullname.strip.gsub(/\s+/, ' ')
   end
+  private :fix_attributes
 
   # Site URL, if specified, must be a valid HTTP(S) URL.
   validates_url :site_url, :if=>:site_url

Added: ode/sandbox/singleshot/app/models/sandwich.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/sandwich.rb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/models/sandwich.rb (added)
+++ ode/sandbox/singleshot/app/models/sandwich.rb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,12 @@
+class Sandwich < Singleshot::Task
+
+  data_fields :bread, :spread, :toppings
+
+  def toppings
+    (data && data['toppings']) || ''
+  end
+
+  validates_presence_of :bread, :on=>:complete, :message=>'Spread without a bread?', :if=>:spread
+  validates_length_of   :toppings, :on=>:complete, :minimum=>1, :message=>'Your sandwich is short on toppings!'
+
+end

Modified: ode/sandbox/singleshot/app/models/stakeholder.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/models/stakeholder.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/models/stakeholder.rb (original)
+++ ode/sandbox/singleshot/app/models/stakeholder.rb Tue Dec 11 15:14:39 2007
@@ -27,13 +27,16 @@
     #
     # 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, :excluded_owner, :admin]
+    ROLES = [:creator, :owner, :potential_owner, :excluded_owner, :observer, :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
+    
+    # Based on PLURAL_ROLES, but takes the pluralized form (i.e. admin becomes admins)
+    PLURALIZED_ROLES = PLURAL_ROLES.map { |role| role.to_s.pluralize.to_sym }
 
   end
 
@@ -47,7 +50,7 @@
   validates_presence_of :person_id
 
   # Enumerated role, see Task::Roles for more details.
-  enumerable :role, ROLES
+  enumerable :role, ROLES, :check_methods=>:true
   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=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/models/task.rb (original)
+++ ode/sandbox/singleshot/app/models/task.rb Tue Dec 11 15:14:39 2007
@@ -4,15 +4,16 @@
 # Table name: tasks
 #
 #  id           :integer(11)   not null, primary key
-#  title        :string(255)   default(""), not null
-#  priority     :integer(1)    default(1), not null
+#  title        :string(255)   
+#  priority     :integer(1)    default(1)
 #  due_on       :date          
 #  status       :integer(2)    default(0), not null
-#  form_url     :string(255)   
-#  view_url     :string(255)   
+#  frame_url    :string(255)   
 #  outcome_url  :string(255)   
 #  outcome_type :string(255)   
-#  cancellation :integer(1)    not null
+#  cancellation :integer(1)    
+#  creator_id   :integer(11)   
+#  owner_id     :integer(11)   
 #  access_key   :string(32)    default(""), not null
 #  data         :text          default(""), not null
 #  version      :integer(11)   default(0), not null
@@ -25,45 +26,67 @@
 
 class Task < ActiveRecord::Base
 
-  # Locking column used for versioning and detecting update conflicts.
-  set_locking_column 'version'
+  class << self
 
-  PRIORITIES = 1..5
+    # Create a reserved task, used as a place holder when creating a task exactly once.
+    # A reserve task has no attributes and does not show up. The first update changes its status to active.
+    def reserve!(admin)
+      create! admin
+    end
 
-  def initialize(attributes = nil)
-    super
-    self.status = :active
-    self.access_key = MD5.hexdigest(OpenSSL::Random.random_bytes(128))
   end
 
-  def to_param
-    "#{id}-#{title.gsub(/[^\w]+/, '-').gsub(/-*$/, '')}"
+  def initialize(attributes = {}) #:nodoc:
+    if attributes.is_a?(Person)
+      super :admins=>attributes
+      self.status = :reserved
+    else
+      super attributes
+      self.status = :active
+    end
+    self.access_key = MD5.hexdigest(OpenSSL::Random.random_bytes(128))
   end
 
-  # Returns an ETag that can identify changes to the task state.  No two tasks will have
-  # the same ETag.  Changing the task state will also change its ETag.
-  def etag
-    MD5.hexdigest("#{id}:#{version}")
+  def to_param #:nodoc:
+    title.blank? ? id.to_s : "#{id}-#{title.gsub(/[^\w]+/, '-').gsub(/-*$/, '')}" unless new_record?
   end
 
-
   # --- Task state ---
 
-  enumerable :status, [:active, :suspended, :completed, :cancelled], :check_methods=>true
-  validates_presence_of :status
+  # Locking column used for versioning and detecting update conflicts.
+  set_locking_column 'version'
+
+  enumerable :status, [:reserved, :active, :suspended, :completed, :cancelled], :check_methods=>true
   attr_protected :status
+  validates_presence_of :status
+
+  # Status starts as reserved or active, and changes from reserved to active on first update.
+  validates_inclusion_of :status, :on=>:create, :in=>[:reserved, :active]
+  before_validation_on_update { |task| task.status = :active if task.reserved? }
+  # Only assigned task can change to completed.
+  validate { |task| task.errors.add :status, 'Only owner can complete task' if task.completed? && task.owner.nil? }
+
+  # Set task status to suspended and back to active.
+  def suspended=(suspended)
+    self.status = suspended ? :suspended : :active if active? || suspended?
+  end
 
+  # Task priority: 1 is the lowest (and default) priority.
+  PRIORITIES = 1..3
+  before_validation { |task| task.priority ||= PRIORITIES.min }
   validates_inclusion_of :priority, :in=>PRIORITIES
 
-  def priority
-    self[:priority] ||= PRIORITIES.min
+  # Returns an ETag that can identify changes to the task state.  No two tasks will have
+  # the same ETag.  Changing the task state will also change its ETag.
+  def etag
+    MD5.hexdigest("#{id}:#{version}")
   end
 
 
   # -- View and perform ---
 
-  validates_url :form_url, :if=>:form_url
-  validates_url :view_url, :if=>:view_url
+  validates_presence_of :title, :frame_url, :unless=>:reserved?
+  validates_url :frame_url, :if=>:frame_url
 
 
   # --- Completion and cancellation ---
@@ -73,15 +96,10 @@
   # Supported formats for updating the outcome resource.
   OUTCOME_MIME_TYPES = [Mime::JSON, Mime::XML]
 
+  before_validation { |task| task.outcome_type = nil unless task.outcome_url }
+  before_validation { |task| task.outcome_type ||= Mime::XML if task.outcome_url }
   validates_inclusion_of :outcome_type, :in=>OUTCOME_MIME_TYPES, :if=>:outcome_url,
     :message=>"Supported MIME types are #{OUTCOME_MIME_TYPES.to_sentence}"
-  before_validation { |record| record.outcome_type ||= Mime::XML if record.outcome_url }
-  before_save { |record| record.outcome_type = nil unless record.outcome_url }
-
-  # Returns the outcome content type.
-  def outcome_type
-    Mime::LOOKUP[self[:outcome_type]]
-  end
 
   # Sets the outcome content type.
   def outcome_type=(mime_type)
@@ -94,21 +112,21 @@
     end
   end
 
-  def complete!(person, data = nil)
-    return false unless can_complete?(person)
+  def complete!(data = nil)
     self.status = :completed
     self.data = data if data
     # TODO: Update outcome, observers
     save!
   end
 
-
-
-  def cancel!(person)
-    return false unless can_cancel?(person)
-    self.status = :cancelled
-    # TODO: Update outcome, observers
-    save!
+  def cancel!
+    if reserved?
+      destroy
+    else
+      self.status = :cancelled
+      # TODO: Update outcome, observers
+      save!
+    end
   end
 
 
@@ -119,15 +137,15 @@
   end
 
   def data=(data)
-    raise ArgumentError, 'Must be a hash' unless Hash === data
+    raise ArgumentError, 'Must be a hash or nil' unless Hash === data || data.nil?
     @data = data
   end
 
-  before_save do |record|
-    record.instance_eval do
-      self[:data], @data = @data.to_json, nil if @data
-    end 
+  before_save :serialize_data
+  def serialize_data
+    self[:data], @data = @data.to_json, nil if @data
   end
+  private :serialize_data
 
 
   # --- Stakeholders ---
@@ -138,92 +156,18 @@
   has_many :stakeholders, :include=>:person, :dependent=>:delete_all
   attr_protected :stakeholders
  
-  class << self
-
-    # Returns list of administrators associated with all tasks.  These are not stored along with
-    # the task, but has the same role as admin stakeholder in each task.
-    def admins
-      @admins ||= []
-    end
-
-    # Sets list of administrators associated with all tasks.
-    def admins=(admins)
-      @admins = Array(admins)
-    end
-
-    # Returns true if this person is an administrator on all tasks.
-    def admin?(person)
-      admins.include?(person)
-    end
-
-  end
-
-  # :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
-  private :in_role
-
-  # :call-seq:
-  #   in_role?(person, *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?(person, *roles)
-    roles = roles.flatten
-    if roles.include?(:any)
-      stakeholders.any? { |sh| sh.person_id == person.id }
-    else
-      stakeholders.any? { |sh| roles.include?(sh.role) && sh.person_id == person.id }
-    end
-  end
-  private :in_role?
-
-  # :call-seq:
-  #   roles(person) => *roles
-  #
-  # Returns all the roles this person is associated with.
-  def roles(person)
-    stakeholders.select { |sh| sh.person.id == person.id }.map { |sh| sh.role }.uniq
-  end
-  private :roles
 
   # Adds methods for singular roles, like this:
   # * owner?(identity) -- Returns true if person is in this role
   # * owner            -- Returns identity of person in this role
-  # * owner = identity -- Assigns a person to this role
+  # * owner = identity -- Assigns 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.blank? && person = identify_or_new(identity)
-        if current.nil? || person != current.person
-          stakeholders.delete current if current
-          stakeholders.build :person=>person, :role=>role
-        end
-      else
-        stakeholders.delete current if current
-      end
-    end
-  end
-
-  validate do |record|
-    record.instance_eval do
-      SINGULAR_ROLES.each do |role|
-        errors.add role, 'A task can have only one #{role}' if in_role(role).size > 1
-      end
+    belongs_to role, :class_name=>'Person'
+    define_method("#{role}?") { |identity| send(role) == Person.identify(identity)  }
+    define_method "#{role}_with_identify=" do |identity|
+      send "#{role}_without_identify=", identity.blank? ? nil : identify_or_create(identity)
     end
+    alias_method_chain "#{role}=", :identify
   end
 
   # Adds methods for plural roles, like this:
@@ -232,20 +176,24 @@
   # * 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 "#{role}?" do |identity|
+      person = Person.identify(identity)
+      stakeholders.any? { |sh| sh.role == role && sh.person_id == person.id }
+    end
+    define_method(plural) { stakeholders.select { |sh| sh.role == role }.map(&:person) }
     define_method "#{plural}=" do |identities|
-      new_set = Array(identities).map { |identity| identify_or_new(identity) }.uniq
+      new_set = Array(identities).map { |identity| identify_or_create(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 }
+      (new_set - existing.map(&:person)).each { |person| stakeholders.build :person=>person, :role=>role }
     end
   end
 
   # Returns true if person is a stakeholder in this task (equivalent to asking if in any role).
   # Includes all global administrators but excludes excluded owners.
   def stakeholder?(person)
-    stakeholders.any? { |sh| sh.person.id == person.id && sh.role != :excluded_owner } || Task.admin?(person)
+    person == owner || person == creator || person.admin? ||
+      stakeholders.any? { |sh| sh.person_id == person.id && sh.role != :excluded_owner }
   end
 
   # Slightly different from Person.identify.  If Person.identify does not find the person,
@@ -253,15 +201,34 @@
   # RecordNotFound exception is also bad, unless we pay attention, we'll end up sending
   # a 404 to the client.  So this method creates a new record in memory, which we're going
   # to detect during validation and fail with a proper error message.
-  def identify_or_new(person)
-    Person.identify(person) || Person.new(:nickname=>person)
+  def identify_or_create(person)
+    Person.identify(person) || Person.new(:identity=>person)
   end
-  private :identify_or_new
+  private :identify_or_create
+  
 
-  validate do |record|
-    record.stakeholders.each do |stakeholder|
-      person = stakeholder.person
-      record.errors.add :stakeholders, "Cannot find person with identity '#{person.to_param}' for the role #{stakeholder.role}" if person.new_record?
+  before_validation do |task|
+    # Delete potential owners listed in the excluded owners list.  Use identity to handle new records.
+    excluded = task.excluded_owners.map(&:identity)
+    task.stakeholders.delete task.stakeholders.select(&:potential_owner?).select { |sh| excluded.include?(sh.person.identity) }
+    # Assign owner if only one potential owner specified.
+    task.owner = task.potential_owners.first if task.owner.nil? && task.potential_owners.size == 1
+  end
+
+  validate do |task|
+    # Complain of stakesholders not in database, which happens when passing identifiers that don't resolve to people.
+    task.stakeholders.select { |sh| sh.person.new_record? }.each do |sh|
+      task.errors.add sh.role, "Cannot find person with identity '#{sh.person.to_param}' for the role #{sh.role}"
+    end
+    SINGULAR_ROLES.each do |role|
+      person = task.send(role)
+      task.errors.add role, "Cannot find person with identity '#{person.to_param}' for the role #{role}" if
+        person && person.new_record?
+    end
+    # Owner can be anyone except excluded owners.
+    if task.owner
+      task.errors.add :owner, "#{task.owner.fullname} is on the excluded owners list and cannot claim this task." if
+        task.excluded_owners.map(&:identity).include?(task.owner.identity)
     end
   end
 
@@ -273,11 +240,8 @@
   # Returns true if this person can cancel this task.
   def can_cancel?(person)
     return false if completed?
-    return true if admin?(person) || Task.admin?(person)
-    case cancellation
-    when :owner
-      return true if owner?(person)
-    end
+    return true if person.admin? || admin?(person)
+    return owner?(person) if cancellation == :owner
     false
   end
 
@@ -286,19 +250,38 @@
     active? && owner?(person)
   end
 
-  def filter_update(person, values)
-    if admin?(person)
-      # Administrator allowed to change all unprotected attributes.
-      values
-    elsif owner?(person)
-      # Owner allowed to change task data and assign to someone else.
-      if new_owner = values[:owner]
-        return nil unless potential_owner?(new_owner) && !excluded_owner?(new_owner)
+  def can_suspend?(person)
+    admin?(person) || person.admin?
+  end
+
+  def can_claim?(person)
+    owner.nil? && potential_owner?(person)
+  end
+
+  def can_assign_to?(person)
+    !excluded_owner?(person)
+  end
+
+  def filter_update_for(person)
+    if admin?(person) || person.admin?
+      # Administrator can change anything, but make sure to retain as administrator,
+      # and limit status change to active/suspended.
+      lambda { |attrs|
+        status = attrs[:status].to_s
+        attrs.update(:suspended=> status == 'suspended') if ['active', 'suspended'].include?(status)
+        attrs.update(:admins=>Array(attrs[:admins]) << person) }
+    elsif active?
+      if owner?(person)
+        # Owner is allowed to change ownership and task data, but only release if there
+        # are other potential owners.  Owner also allowed to change task data.
+        lambda { |attrs|
+          released = attrs.has_key?(:owner) && attrs[:owner].blank?
+          attrs.slice!(:owner, :data) unless released && (potential_owners - [owner]).empty? }
+      elsif potential_owner?(person)
+        # Potential owner allowed to claim unclaimed task.
+        lambda { |attrs|
+          attrs.slice!(:owner) if person.same_as?(attrs[:owner]) && owner.nil? }
       end
-      values.slice(:data, :owner, :version)
-    elsif potential_owner?(person) && owner.nil? && values(:owner) == person.to_param
-      # Potential owner only allowed to claim task if not already assigned.
-      values.slice(:owner, :version)
     end
   end
 
@@ -316,7 +299,7 @@
   # 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 }
+    (stakeholders.map { |sh| sh.person } + [owner, creator].compact).uniq.find { |person| token_for(person) == token }
   end
 
 end

Modified: ode/sandbox/singleshot/app/views/layouts/application.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/layouts/application.html.erb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/views/layouts/application.html.erb (original)
+++ ode/sandbox/singleshot/app/views/layouts/application.html.erb Tue Dec 11 15:14:39 2007
@@ -1,8 +1,9 @@
 <html>
   <head>
-    <title>Single Shot<%= " &ndash; #{escape_once(@title)}" if @title %></title>
+    <title>Single Shot<%= " &mdash; #{escape_once(@title)}" if @title %></title>
     <%= javascript_include_tag :all, :cache=>true %>
-    <%= stylesheet_link_tag :all, :cache=>true %>
+    <%= stylesheet_link_tag 'screen', 'buttons', 'default', :cache=>true %>
+    <%= stylesheet_link_tag 'print', :media=>'print', :cache=>true %>
     <% @alternate.each do |mime, url| %>
       <%= auto_discovery_link_tag mime.to_sym, url %>
     <% end if @alternate %>

Added: ode/sandbox/singleshot/app/views/layouts/head.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/layouts/head.html.erb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/layouts/head.html.erb (added)
+++ ode/sandbox/singleshot/app/views/layouts/head.html.erb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,12 @@
+<html>
+  <head>
+    <title>Single Shot<%= " &mdash; #{escape_once(@title)}" if @title %></title>
+    <%= javascript_include_tag :all, :cache=>true %>
+    <%= stylesheet_link_tag 'screen', 'buttons', 'default', :cache=>true %>
+    <%= stylesheet_link_tag 'print', :media=>'print', :cache=>true %>
+    <% @alternate.each do |mime, url| %>
+      <%= auto_discovery_link_tag mime.to_sym, url %>
+    <% end if @alternate %>
+  </head>
+<%= yield %>
+</html>

Added: ode/sandbox/singleshot/app/views/sandwiches/show.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/sandwiches/show.html.erb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/sandwiches/show.html.erb (added)
+++ ode/sandbox/singleshot/app/views/sandwiches/show.html.erb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,37 @@
+<script>
+  function roomForNewTopping() {
+    var lastTopping = $('last_topping').parentNode;
+    var newTopping = lastTopping.cloneNode(true);
+    newTopping.firstChild.value = null;
+    lastTopping.parentNode.insertBefore(newTopping, lastTopping.nextSibling);
+  }
+</script>
+<div style='padding:2em 5em 2em 5em'>
+  <% form_for :sandwich, @sandwich, :html=>{ :id=>'sandwich' } do |f| %>
+    <%= content_tag 'p', flash[:success], :class=>'success' if flash[:success] %>
+    <%= error_messages_for :sandwich, :message=>"Sudo can't make this sandwich ...", :header_message=>nil, :class=>'error' %>
+    <dl>
+      <dt><%= f.label :bread, 'Pick your bread' %></dt>
+      <dd><%= f.select :bread, ['Crusty', 'Toasty', 'French'].map { |s| [s, s.split.first.downcase] } + [['Atkins style', '']],
+                {}, :disabled=>@read_only %></dd>
+      <dt><%= f.label :spread, 'Choose your spread' %></dt>
+      <dd><%= f.select :spread, ['Creamy', 'Eggy', 'Not butter'].map { |s| [s, s.sub(/ /, '_').downcase] } + [['Air like', '']],
+                {}, :disabled=>@read_only %></dd>
+      <dt><%= f.label :toppings, 'Top it with' %>
+          <%= '(' + link_to_function('add topping', 'roomForNewTopping()') + ')' unless @read_only %></dt>
+      <% @sandwich.toppings.split(/;/).each do |topping| %>
+        <dd><%= text_field_tag 'sandwich[toppings][]', topping, :disabled=>@read_only %></dd>
+      <% end %>
+      <dd><%= text_field_tag 'sandwich[toppings][]', '', :id=>'last_topping', :disabled=>@read_only %></dd>
+    </dl>
+    <% unless @read_only %>
+      <hr>
+      <p>
+        <%= content_tag :button, 'Make!', :class=>'positive' %>
+        <%= content_tag :button, 'Save and continue', :name=>:_method, :value=>:put %>
+      </p>
+    <% end %>
+    <%= hidden_field_tag 'task_url', @task_url %>
+  <% end %>
+  <%= javascript_tag "$('sandwich').focusFirstElement()" %>
+</div>

Modified: ode/sandbox/singleshot/app/views/sessions/show.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/sessions/show.html.erb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/app/views/sessions/show.html.erb (original)
+++ ode/sandbox/singleshot/app/views/sessions/show.html.erb Tue Dec 11 15:14:39 2007
@@ -1,13 +1,13 @@
-<div class='login'>
-  <% form_for :session, :url=>session_url do |f| %>
+<% form_for :session, :url=>session_url, :html=>{ :class=>'login' } do |f| %>
+  <fieldset>
     <%= content_tag 'p', flash[:error], :class=>'error' if flash[:error] %>
     <dl>
       <dt><label for='login'>Login:</label></dt>
       <dd><%= text_field_tag 'login', nil, :size=>30 %></dd>
       <dt><label for='password'>Password:</label></dt>
       <dd><%= password_field_tag 'password', nil, :size=>30 %></dd>
-      <dd><%= submit_tag 'Login' %></dd>
+      <dd><button class='positive'>Login</button></dd>
     </dl> 
-  <% end %>
-</div>
+  </fieldset>
+<% end %>
 <%= javascript_tag "$('login').focus()" %>

Added: ode/sandbox/singleshot/app/views/tasks/new.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/tasks/new.html.erb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/tasks/new.html.erb (added)
+++ ode/sandbox/singleshot/app/views/tasks/new.html.erb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,33 @@
+<div>
+  <h1>Create new task</h1>
+  <% everyone = Person.find(:all).sort_by(&:fullname) %>
+  <% form_for @task, :disabled=>true do |f| %>
+    <%= content_tag 'p', flash[:error], :class=>'error' if flash[:error] %>
+    <table>
+      <tr>
+        <td><%= f.label :title, 'Title:' %></td>
+        <td><%= f.text_field :title, :size=>30 %>
+          <span class='tip'>Short description, shows in task list.</span>
+        </td>
+      </tr>
+      <tr>
+        <td><%= f.label :creator, 'Creator:' %></td>
+        <td><%= hidden_field_tag 'task[creator]', @task.creator.to_param %><%= link_to_person authenticated %>
+          <span class='tip'>That would be you.</span>
+        </td>
+      </tr>
+      <tr>
+        <td><%= f.label :owner, 'Owner:' %></td>
+        <td><%= f.select :owner, [['Later ...', nil]] + (everyone - [authenticated]).map { |p| [p.fullname, p.nickname] } %>
+          <span class='tip'>Select task owner now, or let someone else claim the task later.</span>
+        </td>
+      </tr>
+      <tr>
+        <td></td>
+        <td><%= f.submit 'Create' %></td>
+      </tr>
+    </table>
+    <%= everyone.map { |person| hidden_field_tag 'task[potential_owners][]', person.to_param, :id=>nil } %>
+  <% end %>
+</div>
+<%= javascript_tag "$('task_title').focus()" %>

Added: ode/sandbox/singleshot/app/views/tasks/show.html.erb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/app/views/tasks/show.html.erb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/app/views/tasks/show.html.erb (added)
+++ ode/sandbox/singleshot/app/views/tasks/show.html.erb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,15 @@
+<% @title = @task.title %>
+<body id='task'>
+  <div id='task_bar'>
+    <div id='summary'>
+      <span class='column prepend-1'><%= link_to 'Singleshot', root_url, :title=>'Switch back to dashboard' %></span>
+      <span class='column prepend-1'><%= task_bar_vitals(@task) %></span>
+      <span class='column push-0 append-1 last'><%= task_bar_actions(@task) %> <%= link_to_function 'More Options', "Singleshot.expand(event, 'expanded')" %></span>
+    </div>
+    <div id='expanded' style='display:none'>
+      Nothing to see ... move right along.
+    </div>
+  </div>
+  <%= content_tag 'iframe', nil, :id=>'task_frame', :src=>task_iframe_url(@task) %>
+  <%= javascript_tag 'Singleshot.taskView()' %>
+</body>

Modified: ode/sandbox/singleshot/config/environment.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/environment.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/environment.rb (original)
+++ ode/sandbox/singleshot/config/environment.rb Tue Dec 11 15:14:39 2007
@@ -42,13 +42,13 @@
   # Use SQL instead of Active Record's schema dumper when creating the test database.
   # This is necessary if your schema can't be completely dumped by the schema dumper,
   # like if you have constraints or database-specific column types
-  # config.active_record.schema_format = :sql
+  config.active_record.schema_format = :sql
 
   # Activate observers that should always be running
   # config.active_record.observers = :cacher, :garbage_collector
 
   # Make Active Record use UTC-base instead of local time
-  # config.active_record.default_timezone = :utc
+  config.active_record.default_timezone = :utc
 
   # See Rails::Configuration for more options
 

Modified: ode/sandbox/singleshot/config/environments/development.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/environments/development.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/environments/development.rb (original)
+++ ode/sandbox/singleshot/config/environments/development.rb Tue Dec 11 15:14:39 2007
@@ -16,3 +16,5 @@
 
 # Don't care if the mailer can't send
 config.action_mailer.raise_delivery_errors = false
+
+config.action_controller.allow_concurrency = true

Modified: ode/sandbox/singleshot/config/initializers/libs.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/initializers/libs.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/initializers/libs.rb (original)
+++ ode/sandbox/singleshot/config/initializers/libs.rb Tue Dec 11 15:14:39 2007
@@ -1,2 +1,4 @@
-require 'open-uri'
+require 'rest-open-uri'
+require File.join(RAILS_ROOT, 'lib/patches')
 require File.join(RAILS_ROOT, 'lib/extensions')
+require File.join(RAILS_ROOT, 'lib/singleshot')

Added: ode/sandbox/singleshot/config/lighttpd.conf
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/lighttpd.conf?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/config/lighttpd.conf (added)
+++ ode/sandbox/singleshot/config/lighttpd.conf Tue Dec 11 15:14:39 2007
@@ -0,0 +1,54 @@
+# Default configuration file for the lighttpd web server
+# Start using ./script/server lighttpd
+
+server.bind = "0.0.0.0"
+server.port = 3000
+
+server.modules           = ( "mod_rewrite", "mod_accesslog", "mod_fastcgi", "mod_compress", "mod_expire" )
+
+server.error-handler-404 = "/dispatch.fcgi"
+server.pid-file          = CWD + "/tmp/pids/lighttpd.pid"
+server.document-root     = CWD + "/public/"
+
+server.errorlog          = CWD + "/log/lighttpd.error.log"
+accesslog.filename       = CWD + "/log/lighttpd.access.log"
+
+url.rewrite              = ( "^/$" => "index.html", "^([^.]+)$" => "$1.html" )
+
+compress.filetype        = ( "text/plain", "text/html", "text/css", "text/javascript" )
+compress.cache-dir       = CWD + "/tmp/cache"
+
+expire.url               = ( "/favicon.ico"  => "access 3 days", 
+                             "/images/"      => "access 3 days", 
+                             "/stylesheets/" => "access 3 days",
+                             "/javascripts/" => "access 3 days" )
+
+
+# Change *-procs to 2 if you need to use Upload Progress or other tasks that
+# *need* to execute a second request while the first is still pending.
+fastcgi.server      = ( ".fcgi" => ( "localhost" => (
+  "min-procs"       => 1, 
+  "max-procs"       => 5,
+  "socket"          => CWD + "/tmp/sockets/fcgi.socket",
+  "bin-path"        => CWD + "/public/dispatch.fcgi",
+  "bin-environment" => ( "RAILS_ENV" => "development" )
+) ) )
+
+mimetype.assign = (  
+  ".css"        =>  "text/css",
+  ".gif"        =>  "image/gif",
+  ".htm"        =>  "text/html",
+  ".html"       =>  "text/html",
+  ".jpeg"       =>  "image/jpeg",
+  ".jpg"        =>  "image/jpeg",
+  ".js"         =>  "text/javascript",
+  ".png"        =>  "image/png",
+  ".swf"        =>  "application/x-shockwave-flash",
+  ".txt"        =>  "text/plain"
+)
+
+# Making sure file uploads above 64k always work when using IE or Safari
+# For more information, see http://trac.lighttpd.net/trac/ticket/360
+$HTTP["useragent"] =~ "^(.*MSIE.*)|(.*AppleWebKit.*)$" {
+  server.max-keep-alive-requests = 0
+}

Modified: ode/sandbox/singleshot/config/routes.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/config/routes.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/config/routes.rb (original)
+++ ode/sandbox/singleshot/config/routes.rb Tue Dec 11 15:14:39 2007
@@ -2,8 +2,12 @@
 
   map.resource :session
   map.resources :tasks
-  map.connect '/tasks/:id', :controller=>'tasks', :action=>'complete', :id=>/\d+/, :conditions=>{ :method=>:post }
-  map.connect '/tasks/:id.format', :controller=>'tasks', :action=>'complete', :id=>/\d+/, :conditions=>{ :method=>:post }
+  map.with_options :controller=>'tasks', :action=>'complete', :conditions=>{ :method=>:post },
+                   :id=>/[^#{ActionController::Routing::SEPARATORS.join}]+/ do |task|
+    task.connect '/tasks/:id'
+    task.connect '/tasks/:id.:format'
+  end
+  map.resource :sandwich
   map.root :controller=>'tasks'
 
   # Authentication resource (create/destroy).

Modified: ode/sandbox/singleshot/db/migrate/001_create_people.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/db/migrate/001_create_people.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/db/migrate/001_create_people.rb (original)
+++ ode/sandbox/singleshot/db/migrate/001_create_people.rb Tue Dec 11 15:14:39 2007
@@ -1,7 +1,7 @@
 class CreatePeople < ActiveRecord::Migration
   def self.up
     create_table :people do |t|
-      t.string  :nickname,    :null=>false
+      t.string  :identity,    :null=>false
       t.string  :fullname,    :null=>false
       t.string  :email,       :null=>false
       t.string  :language,    :null=>true,  :limit=>5
@@ -9,9 +9,10 @@
       t.string  :password,    :null=>true,  :limit=>64
       t.string  :access_key,  :null=>false, :limit=>32
       t.string  :site_url,    :null=>true
+      t.boolean :admin,       :null=>false, :default=>false
       t.timestamps
     end
-    add_index :people, :nickname, :unique=>true
+    add_index :people, :identity,   :unique=>true
     add_index :people, :fullname
     add_index :people, :email,      :unique=>true
     add_index :people, :access_key, :unique=>true

Modified: 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=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/db/migrate/002_create_tasks.rb (original)
+++ ode/sandbox/singleshot/db/migrate/002_create_tasks.rb Tue Dec 11 15:14:39 2007
@@ -1,15 +1,16 @@
 class CreateTasks < ActiveRecord::Migration
   def self.up
     create_table :tasks do |t|
-      t.string    :title,        :null=>false
-      t.integer   :priority,     :null=>false, :default=>1, :limit=>1
+      t.string    :title,        :null=>true
+      t.integer   :priority,     :null=>true, :default=>1, :limit=>1
       t.date      :due_on,       :null=>true
       t.integer   :status,       :null=>false, :default=>0, :limit=>2
-      t.string    :form_url,     :null=>true
-      t.string    :view_url,     :null=>true
+      t.string    :frame_url,    :null=>true
       t.string    :outcome_url,  :null=>true
       t.string    :outcome_type, :null=>true
-      t.integer   :cancellation, :null=>false, :limit=>1
+      t.integer   :cancellation, :null=>true, :limit=>1
+      t.integer   :creator_id,   :null=>true 
+      t.integer   :owner_id,     :null=>true 
       t.string    :access_key,   :null=>false, :limit=>32
       t.text      :data,         :null=>false
       t.integer   :version,      :null=>false, :default=>0

Modified: ode/sandbox/singleshot/doc/pages/managing_tasks.textile
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/doc/pages/managing_tasks.textile?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/doc/pages/managing_tasks.textile (original)
+++ ode/sandbox/singleshot/doc/pages/managing_tasks.textile Tue Dec 11 15:14:39 2007
@@ -5,136 +5,169 @@
 To create a new task, the application authenticates and sends a POST request to the resource @/tasks@.  The task manager returns one of the following responses:
 
 * *201 (Created)* -- Includes a representation of the task in the selected content type, and the task URL in the @Location@ header.
-* *400 (Bad Request)* -- Request missing the @<task>@ element (XML) or JSON object.
-* *422 (Unprocessable Entity)* -- Indicates an invalid request, for example, person that does not exist, priority out of range.  The response provides more details.
+* *422 (Unprocessable Entity)* -- Indicates an invalid request, for example, priority out of range, person does not exist, etc.  The response provides more details.
 
-The application can make the request in any of the supported content types (see "Content Types":#Content-Types), using the following fields:
+The application can make the request in any of the supported content types (see "Content Types":#content_types), using the following fields:
 
-* @admins@ -- List of administrators associated with the task.  You can use this to associate specific administrators on a task, beyond those allowed to administer all tasks.  The account used to create the task is automatically added as an administrator on the task.
-* @cancellation@ -- Indicates which roles are able to cancel the task.
-* @creator@ -- When creating the task on behalf of a person, use this to specify the creator's identity.  The creator of the task is separate from the account used to create the task, the later typically belonging to the application.
-* @data@ -- Each task can hold an arbitrary set of name/value pairs which are passed between the application and task manager, made available when viewing/performing the task, and passed back to the application on completion.
-* @due_on@ -- If the task has a due-on date, this specifies the date/time using the ISO8601 format.
-* @excluded_owners@ -- List of people who are not allowed to claim the task, typically owners of previously completed tasks.  People listed as both potential and excluded owners are not allowed to claim the task.
-* @form_url@ -- URL for the form represented by the task.
-* @outcome_url@ -- When the task completes, the task manager updates the outcome resource; it deletes the outcome resource if the task is cancelled.
-* @owner@ -- Specify this to immediately assign the task to a particular person.  Otherwise, the task is left available and any potential owner can claim it.
+* @admins@ -- List of administrators associated with the task.  You can use this to associate specific administrators on the individual task.  The account used to create the task is automatically added as an administrator during creation.
+* @cancellation@ -- Indicates which roles are allowed to cancel the task.
+* @creator@ -- Use this when creating a task on behalf of a person.  The creator of the task is separate from the account used to create the task, the later typically authenticating the application.
+* @data@ -- Each task can hold a set of name/value pairs which are passed from the application to the viewer and back when completing the task.
+* @due_on@ -- If the task has a due-on date.
+* @excluded_owners@ -- List of people who are not allowed to claim the task, typically owners of previously completed tasks.  People listed as excluded owners are not allowed to view or claim the task.
+* @frame_url@ -- URL for use as a frame when viewing/performing the task.
+* @outcome_url@ -- The task manager updates this URL when the task completes or is cancelled.
+* @owner@ -- Specify this to immediately assign the task to a particular person, otherwise, any potential owner can claim the task as their own.
 * @potential_owners@ -- List of potential owners that can claim the task.
-* @priority@ -- The task priority as a value from 1 to 5, 5 being the highest priority.
-* @title@ --The task title as it will show up in the tasks list.  The task title is assumed to be text, UTF-8 accepted.
-* @view_url@ -- URL for viewing the task, alternative to @form_url@.
+* @priority@ -- The task priority as a value from 1 to 3, 1 being the lowest (and default) priority.
+* @title@ -- The task title as it shows in the tasks list.  The task title is assumed to be text.
 
-When specifying a singular stakeholder (e.g. creator, owner), use the form of identity associated with that person, currently their nickname.  When specifying a plural stakeholder (e.g. potential owners, admins), use an array of identities.  Either way, the person referenced as a stakeholder must exist in the system with that identity.
+To specify a singular stakeholder (e.g. creator, owner), use the form of public identity associated with that person, for example, nickname or OpenID URL.  To specify a plural stakeholder (e.g. potential owners, admins), use an array of identities.  See "Content Types":#content_types for details on how to specify an array, and make sure to include the @type='array'@ attribute when using XML.
 
-In JSON, the task data is represented as a hash.  In XML, as an element called @data@, with each name/value pair represented as an element of that name and value as the element content.  In URL encoded and multi-part, using the notation for hashs.  For example:
+In both cases, the person must exist in the system before they can be referenced as a stakeholder, otherwise the request will fail.
+
+In JSON, the task data is represented as a hash, for example:
 
   task {
+    title: 'Close order',
     data: {
       order: 123,
       status: "open"s
     }
   }
 
+In XML, the element @data@ is structured to contain one child element for each name/value pair, for example:
+
   <task>
+    <title>Close order</title>
     <data>
       <order type='integer'>123</order>
       <status>open</status>
     </data>
   </task>
 
-  task[data][order]=123&task[data][status]=open
-
-
-h3.  Creating a Task Exactly Once
-
-1.  POST to create a new task resource.
-2.  Save the resource URL and ETag.
-3.  PUT to update the task resource using using the ETag.
+In URL-encoded and multi-part, use the notation for representing hashes, for example:
 
-You can repeat the last step any number of times, for example, after recovery from failure.  By setting the @If-Match@ header to the value of the @ETag@ header returned in step 2, you are guaranteed to only change the task from its original state.  Once the task changed from its initial state, the request will return 412 (Precondition Failed).
+  task[title]=Close+order&task[data][order]=123&task[data][status]=open
 
-The first two steps can be repeated any number of times, in case of failure, leading to redundant tasks, but tasks that are never performed and could safely be purged at a later time.
 
+h2.  Changing and Cancelling
 
-h2.  Controlling the Task
+Once the application created the task, the resource can be used to read and change the task state, including cancelling and completing the task.  Any stakeholder is able to view the task state, other operations are access controlled.  For example, only an administrator is allowed to change the task title, and only the owner is allowed to complete the task.
 
-Once the application created the task resource, it can use that URL to read and update the task state, and also to cancel the task.  The application can do so by virtue of being an administrator on the task, so it must use the same credentials it originally used to create the task.
+The application can read and update the task state and also cancel/delete it by virtue of being an administrator on the task, so it must use the same credentials when accessing the task after creation.
 
 
-h3.  Reading
+h3.  Reading the Task State
 
-To read the task state, the application authenticates and sends a GET request to the task resource.  The task manager returns one of the following responses:
+To read the task state, authenticate and send a GET request to the task resource.  The task manager returns one of the following responses:
 
 * *200 (OK)* -- A representation of the task in the requested content type.
-* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account is now allowed to view this task.
+* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account cannot view this task.
 
 The task representation includes the following fields:
 
-* @id@ -- The task identifier.
-* @url@ -- URL for the task resource (this is used primarily for collections).
-* @status@ -- The task status, one of @active@, @suspended@, @completed@ or @cancelled@.
-* @title@ --The task title.
-* @priority@ -- The task priority.
-* @due_on@ -- The task due-on date, if specified.
+* @admins@ -- List of administrators associated with this task.
+* @created_at@ -- Timestamp set at creation.
 * @creator@ -- The task creator, if specified.
+* @data@ -- Name/value pairs.
+* @due_on@ -- The task due-on date, if specified.
+* @id@ -- The task identifier.
 * @owner@ -- The task owner, if the task is assigned.
 * @potential_owners@ -- List of potential owners associated with this task.
-* @admins@ -- List of administrators associated with this task.
-* @data@ -- Name/value pairs.
-* @version@ -- Version number, incremented on each update.
-* @created_at@ -- Timestamp set at creation.
+* @priority@ -- The task priority.
+* @status@ -- The task status, one of @active@, @suspended@ or @completed@.
+* @title@ --The task title.
 * @updated_at@ -- Timestamp of the last update (or creation).
 
-Each change to the task state increments the @version@ number and updates the @updated_at@ timestamp.  You can use these fields to detect changes in the task state.  You can also use the @ETag@ and @Last-Modified@ headers to perform conditional GET and PUT.
+Each change to the task state updates the @updated_at@ timestamp.  You can also use the @ETag@ and @Last-Modified@ headers to detect changes and to make conditional GET and PUT requests.
 
 
-h3. Updating
+h3.  Changing the Task State
 
-To update the task, the application authenticates and sends a PUT request to the task resource.  The task manager returns one of the following responses:
+To change the task state, authenticate and send a PUT request to the task resource.  The task manager returns one of the following responses:
 
 * *200 (OK)* -- Indicates successful update and returns a new representation of the task in the requested content type.
-* *403 (Forbidden)* -- This account is not allowed to change the task, or change a particular field in the task (for example, owners are not allowed to change the task title).
-* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account is now allowed to view this task.
-* *400 (Bad Request)* -- Request missing the @<task>@ element (XML) or JSON object.
+* *403 (Forbidden)* -- The authenticated account is now allowed to make certain changes (for example, only administrators are allowed to change the task title).
+* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account cannot view this task.
+* *400 (Bad Request)* -- Request missing the XML @<task>@ element or JSON object.
+* *409 (Conflict)* -- Task already completed or other conflicting change since the last read.
 * *422 (Unprocessable Entity)* -- Indicates an invalid request, for example, setting the owner to a non-existent person.  The response provides more details.
-* *412 (Percondition Failed)* -- Used with conditional PUT to indicate conflicting update.
+* *412 (Percondition Failed)* -- Returned from a conditional PUT to indicate conflicting update.
 
-Updates can specify all the fields provided at task creation.  Updates can send additional fields provided in the task representation, such as @id@ and @url@, but these are ignored.  Which fields get updated depends on the role of the authenticated account, specifically:
+A change can affect any of the fields specified at creation, subject to access control, specifically:
 
-* Administrators can change all fields in the task state.
-* Owners can change the task data, and either assign the task to another potential owner, or release the task by setting the owner to nil.
+* Administrators can change all the fields specified at creation.
+* Owners can change the task data, assign the task to another owner, or release the task by setting the owner to nil.
 * Potential owners can assign the task to themselves if the task has no current owner.
 
-To change a particular field, the request must include a new value for that field.  Fields that do not appear in the request, do not change.  To remove the due date or current owner, pass an empty string for the value of that field.  To delete plural fields, such as @potential_owners@, pass an empty array.
+Certain fields are available in the task representation but not used at creation, for example, @id@ and @updated_at@.  These are read-only fields that are silently ignored in the request, allowing the application to retrieve and update the same document entity.  Fields that are missing in the request are also ignored, the only way to change the value of a field is by specifying a new value.  For example, to remove the due-on date or current owner, pass an empty string for the value of that field.  To delete plural fields, such as @potential_owners@, pass an empty array.
+
+It is possible that this change conflicts with another change made since the last attempt to read the task state.  For example, the task has since been completed and cannot be changed.  The application should read the task state before making another attempt.  If the task manager recognizes the conflict it returns the 409 status code.  When using conditional PUTs, the 412 status code takes precedence over the 409 status code.
+
+Conditional PUTs using ETag/If-Match are the recommended way to detect conflicting updates.
 
 
-h3. Cancelling
+h3.  Cancelling and Deleting
 
-To cancel the task, the application authenticates and sends a DELETE request to the task resource.  The task manager returns one of the following responses:
+To cancel the task, authenticate and send a DELETE request to the task resource.  The task manager returns one of the following responses:
 
-* *200 (OK)* -- Indicates the task has been successfull cancelled.
-* *403 (Forbidden)* -- This account is not allowed to cancel the task.
-* *404 (Not Found)* -- The task does not exist, has already been cancelled, or the authenticated account is now allowed to view this task.
+* *200 (OK)* -- Indicates the task has been cancelled/deleted.
+* *403 (Forbidden)* -- The authenticated account is not allowed to cancel this task.
+* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account cannot view this task.
+* *409 (Conflict)* -- Task already completed.
 
 Cancelling the task depends on the cancellation policy:
 
 * @admin@ -- Only administrators can cancel the task (default).
 * @owner@ -- Task owner can cancel the task.
 
+The difference between cancelling and deleting depends on the task status.  If the task is active or suspended, cancelling the task will also affect the outcome resource.  Otherwise, the task resource is deleted.
+
+
+h3.  Completing the Task
+
+To complete the task, authenticate and send a POST request to the task resource.  The task manager returns one of the following responses:
+
+* *200 (OK)* -- Indicates the task has been cancelled/deleted.
+* *403 (Forbidden)* -- The authenticated account is not allowed to complete this task.
+* *404 (Not Found)* -- The task does not exist, has been cancelled, or the authenticated account cannot view this task.
+* *409 (Conflict)* -- Task already completed.
+
+To avoid conflicts, only the task owner is allowed to complete the task.
+
+
+h2.  Creating a Task Once
+
+Failure could lead to the application creating the same task more than once.  Applications can get around this using the following algorithm:
+
+1.  Authenticate and send a POST request with no document entity to "reserve" a new resoure.
+2.  Extract the resource URL from the @Location@ header.
+3.  Save the resource URL, or retrieve the saved resource URL after failure.
+4.  Authenticate with the same credentials.
+5.  Set the @If-None-Match@ header to @"*"@.
+6.  Send a PUT request with the document entity describing the task.
+
+The first three steps provides a resource URL for the new task, but the task will not show up in any query.  The resource will remain valid for a significant duration of time sufficient to complete the following steps. If failure occurs before completing the third step, the application can start again with the first step with no side effects.
+
+The application performs the last three steps to ensure the task is created exactly once, immediately following the first three steps or after recovery, and can be repeated any number of times.  Authentication with the same credentials is required.  The @If-None-Match@ header guarantees that the first attempt to update the task returns 200 (OK), but any subsequent attempt returns 412 (Precondition Failed) without changing the resource.  The application should treat either status code as a successful response.
+
 
 h2.  Task Outcome
 
-The application can determine the task outcome in three ways.  First, by retrieving the task state and waiting for the task to tranisition into the @completed@ status, or a 404 indicating the task has been cancelled (deleted).  Second, by retriving a list of recently updated tasks and inspecting their state.  This form of update feeds, which also include dedicated feeds for completed and cancelled tasks, are described in the next section.
+The application can determine the task outcome in three ways.
 
-The application can also create an *outcome resource* and wait for the task manager to update it.  Before creating the task, the application establishes a resource that represents the task outcome.  It passes the resource URL in the @outcome_url@ field, when creating the task.
+*Poll*  The application can retrieve the task state periodically and notice changes to the task status.  When the task completes, its status changes to @completed@.  If the task is cancelled, the application receives a 404 status code.
 
-When the task completes, the task manager updates the outcome resource and puts the final representation of the task in there.  If the task was created using the content type @application/xml@ or @application/json@, that same content type is used to update the outcome resource.
+*Feed*  The application can retrieve a feed with all the recent updates and determine the status of each task listed there.  The general update feed (@/tasks@) lists all updates including task completion, but does not list cancelled tasks.  The completion feed (@/tasks/completed@) lists only completed tasks, while the cancellation feed (@/tasks/cancelled@) lists all cancelled tasks.
 
-If the task is cancelled, the task manager discards the outcome resource by deleting it.  The task manager does this even if the application itself cancelled the task.
+*Outcome resource*  The application can specify an outcome resource that the task manager will update with the task outcome.  When the task completes, the task manager updates the outcome resource by sending a PUT request with the final representation of the task.  If the task is cancelled, the task manager deletes the outcome resource by sending a DELETE request.
 
-To deal with network failures, the task manager repeats the request until it gets a satisfactory outcome.  It considers any 2xx status code, 404, 410 or 412 to indicate successful update.  (412 is accepted since the PUT is conditional, to allow once-only update)
+The task manager updates the outcome resource using the same content type used to create the task, either @application/json@ or @application/xml@.
 
-The @outcome_url@ field specifies an HTTP or HTTPS (recommended) URL.  If the URL includes the authority component, the request uses HTTP authentication (Basic).  For example:
+To deal with network failures, the task manager repeats the request until it gets a satisfactory outcome.  When updating the outcome resource, a satisfactory outcome is any 2xx status code, 404, 410 or 412.  When deleting the outcome resource, a satisfactory outcome is any 2xx or 4xx status code.  Updates use conditional PUT with the @If-None-Match@ header set to @"*"@.
+
+The outcome resource URL may use HTTP or HTTPS (recommended). If it includes an authority component, the request uses HTTP Basic Authentication to access the resource.  For example:
 
   https://token:3032ad6aed6c5c3cda992d241f4d28bf@app.example.com/outcome/123
 
@@ -145,19 +178,19 @@
 
 *application/json*  The request is formatted as a JSON object, using arrays and primitive types where applicable.  Use ISO-8601 for date and date/time values.  The same rules apply to responses containing @application/json@.
 
-*application/xml*  The request is formatted as an XML document using the applicable document element, for example, @<task>@ to represent a task.  Element names use dashes instead of underscore, for example, the field @due_on@ becomes the element @<due-on>@.  Simple value use elements, carrying the value in the element contents.  The @type@ attribute specifies the content type.
+*application/xml*  The request is formatted as an XML document using the applicable document element, for example, @<task>@ to represent a task.  Element names use dashes instead of underscores, for example, the field @due_on@ becomes the element @<due-on>@.  Simple value use elements, carrying the value in the element contents.  The @type@ attribute specifies the content type.
 
 The following simple types are supported: 
 
-* @date@ -- Date instance, using XML Schema format.
-* @datetime@ -- Date/time instance, using the XML Schema format.
+* @date@ -- Date instance, using ISO-8601.
+* @datetime@ -- Date/time instance, using ISO-8601.
 * @integer@ -- Signed integer.
 * @float@ -- Numeric float.
-* @boolean@ -- Either @true@ or @false@.
-* @string@ -- Optional, elements without @type@ are interpreted as strings.
+* @boolean@ -- Either @"true"@ or @"false"@.
+* @string@ -- Optional; elements without a @type@ attribute are interpreted as strings.
 * @base64Binary@ -- Base-64 encoded binary content.
 
-Arrays are specified using a plural element with the type @array@, and one singular element for each array item.  For example:
+Arrays are specified using a plural element with the type @"array"@, and one singular element for each array item.  For example:
 
   <admins type='array'>
     <admin>assaf</admin>
@@ -166,9 +199,11 @@
 
 The same rules apply to responses containing @application/xml@.
 
-*application/x-www-form-urlencoded* and *multipart/form-data*  Control names (aka form fields) use a specific naming convention to map them into hashes and arrays.  Brackets are used to denote entry in a hash, for example, @task[priority]@ represents the priority of a task.  Empty brackets as a suffix denote an array, for example, @task[admins][]@.  Use multiple controls, one for each array item.
+*application/x-www-form-urlencoded* and *multipart/form-data*  Control names (aka form fields) use a specific naming convention to map them into hashes and arrays.  Hash entries use brackets to specify the key, for example, @task[priority]@ refers to the task's priority field.  Array items use empty brackets as a suffix, for example, @task[admins][]@ adds an item to the administrators list.  Multiple array values are encoded as multiple name/value pairs.
 
 *application/atom+xml* TBD
 
 *text/calendar* TBD
+
+*Encoding*  All request formats support UTF-8 encoding, all responses use UTF-8 encoding.
 

Modified: ode/sandbox/singleshot/lib/extensions.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/lib/extensions.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/lib/extensions.rb (original)
+++ ode/sandbox/singleshot/lib/extensions.rb Tue Dec 11 15:14:39 2007
@@ -1 +1,16 @@
-Dir[File.join(File.dirname(__FILE__), 'extensions/*.rb')].each { |file| require file }
+require File.expand_path('extensions/instance', File.dirname(__FILE__))
+require File.expand_path('extensions/json_request', File.dirname(__FILE__))
+
+ActionController::Base.class_eval do
+  include ActionController::Instance
+  include ActionController::JsonRequest
+end
+
+require File.expand_path('extensions/enumerable', File.dirname(__FILE__))
+require File.expand_path('extensions/validators', File.dirname(__FILE__))
+
+ActiveRecord::Base.class_eval do
+  include ActiveRecord::Enumerable
+  include ActiveRecord::Validators::Url
+  include ActiveRecord::Validators::Email
+end

Modified: ode/sandbox/singleshot/lib/extensions/enumerable.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/lib/extensions/enumerable.rb?rev=603406&r1=603405&r2=603406&view=diff
==============================================================================
--- ode/sandbox/singleshot/lib/extensions/enumerable.rb (original)
+++ ode/sandbox/singleshot/lib/extensions/enumerable.rb Tue Dec 11 15:14:39 2007
@@ -1,6 +1,11 @@
 module ActiveRecord
-  class Base
-    class << self
+  module Enumerable
+
+    def self.included(mod)
+      mod.extend ClassMethods
+    end
+
+    module ClassMethods
 
       # Handles the named attribute as enumeration using the specified symbols.
       # For example:
@@ -49,7 +54,7 @@
           end
         end
         define_method "#{attr_name}=" do |value|
-          write_attribute attr_name, symbols.index(value || options[:default])
+          write_attribute attr_name, symbols.index(value ? value.to_sym : options[:default])
         end
         # Class method to convert symbol into index.
         class << self ; self ; end.instance_eval do

Added: ode/sandbox/singleshot/lib/extensions/instance.rb
URL: http://svn.apache.org/viewvc/ode/sandbox/singleshot/lib/extensions/instance.rb?rev=603406&view=auto
==============================================================================
--- ode/sandbox/singleshot/lib/extensions/instance.rb (added)
+++ ode/sandbox/singleshot/lib/extensions/instance.rb Tue Dec 11 15:14:39 2007
@@ -0,0 +1,42 @@
+module ActionController
+  module Instance
+    def self.included(mod)
+      mod.extend ClassMethods
+    end
+
+    module ClassMethods
+      def instance(*args)
+        options = args.extract_options!
+        symbol = (args.shift || self.name.demodulize.gsub(/Controller$/, '').underscore).to_sym
+        ivar = "@#{symbol}".to_sym
+        # Define method to retrieve instance value, add as helper.
+        define_method(name) { instance_variable_get(ivar) }
+        protected name
+        helper_method name
+        # Protected method instance() retrieves the current instance of looks it up.
+        cls = symbol.to_s.classify.constantize
+        define_method(:instance) { instance_variable_get(ivar) || instance_variable_set(ivar, cls.find(params['id'])) }
+        protected :instance
+        # Create before_filter using only/except/if options.  Optional check runs on found instance.
+        if check = options.delete(:check)
+          if check.is_a?(Symbol)
+            method = check
+            filter = lambda do |controller|
+              instance = controller.send(:instance)
+              controller.send(check, instance) or raise ActiveRecord::RecordNotFound
+            end
+          else
+            raise ArgumentError, 'The :check option must be a symbol, method or proc' unless check.respond_to?(:call)
+            filter = lambda do |controller|
+              instance = controller.send(:instance)
+              check.call(controller, instance) or raise ActiveRecord::RecordNotFound
+            end
+          end
+          before_filter filter, options
+        else
+          before_filter :instance, options
+        end
+      end
+    end
+  end
+end