diff --git a/README b/README
index fcc1d7a8..d2c82d58 100644
--- a/README
+++ b/README
@@ -9,19 +9,16 @@
* Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss
* Original developer: bsag (http://www.rousette.org.uk/)
* Contributors: http://getontracks.org/wiki/Contributors
-* Version: 2.0RC1
+* Version: 2.0devel
* Copyright: (cc) 2004-2010 rousette.org.uk.
* License: GNU GPL
-All the documentation for Tracks can be found within the /doc directory. It contains a manual in HTML (manual.html) or PDF format (manual.pdf), and this includes full instructions for both new installations and upgrades from older installations of Tracks. The instructions might appear long and intimidatingly complex, but that is mostly because of the number of different platforms supported, and the different configurations which can be used (e.g. running Tracks on your local computer or on a remote server). If you choose the appropriate section for your situation (installation vs. upgrade), and use the easiest (recommended) method, you should find the instructions easy to follow. If you encounter problems, try searching the wiki, forum or mailing list (URLs above), and ask a question if you cannot find a solution to your problem.
+All the documentation for Tracks can be found within the /doc directory and at http://bsag.github.com/tracks/
+The latter includes full instructions for both new installations and upgrades from older installations of Tracks.
+The instructions might appear long and intimidatingly complex, but that is mostly because of the number of different platforms supported, and the different configurations which can be used (e.g. running Tracks on your local computer or on a remote server). If you choose the appropriate section for your situation (installation vs. upgrade), and use the easiest (recommended) method, you should find the instructions easy to follow. If you encounter problems, try searching the wiki, forum or mailing list (URLs above), and ask a question if you cannot find a solution to your problem.
-If you checked out Tracks from the GitHub repository, the manual is not provided by default and is in its own git submodule. To checkout the manual's source files, type "git submodule init doc/manual & git submodule update doc/manual". From then on, you should be able to issue the command "git pull" in the doc/manual directory to update the manual with the latest changes.
-
-The latest version of the manual can be found at http://bsag.github.com/tracks/
-
-For those upgrading, change notes are available in /doc/CHANGELOG. If you are thinking about contributing towards the development of Tracks, please read /doc/README_DEVELOPERS for general information, or /doc/tracks_api_wrapper.rb for information on Tracks' API.
+If you are thinking about contributing towards the development of Tracks, please read /doc/README_DEVELOPERS for general information, or /doc/tracks_api_wrapper.rb for information on Tracks' API.
While fully usable for everyday use, Tracks is still a work in progress. Make sure that you take sensible precautions and back up all your data frequently, taking particular care when you are upgrading.
-Enjoy being productive!
-
+Enjoy being productive!
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 779f9031..f2e2b1ae 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -4,19 +4,6 @@
require_dependency "login_system"
require_dependency "tracks/source_view"
-require "redcloth"
-
-require 'date'
-require 'time'
-
-# Commented the following line because of #744. It prevented rake db:migrate to
-# run because this tag went looking for the taggings table that did not exist
-# when you feshly create a new database Old comment: We need this in development
-# mode, or you get 'method missing' errors
-#
-# Tag
-
-class CannotAccessContext < RuntimeError; end
class ApplicationController < ActionController::Base
@@ -24,26 +11,19 @@ class ApplicationController < ActionController::Base
helper :application
include LoginSystem
- helper_method :current_user, :prefs
+ helper_method :current_user, :prefs, :format_date, :markdown
layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
exempt_from_layout /\.js\.erb$/
-
-
+
before_filter :set_session_expiration
before_filter :set_time_zone
before_filter :set_zindex_counter
before_filter :set_locale
prepend_before_filter :login_required
prepend_before_filter :enable_mobile_content_negotiation
- # after_filter :set_locale
after_filter :set_charset
- include ActionView::Helpers::TextHelper
- include ActionView::Helpers::SanitizeHelper
- extend ActionView::Helpers::SanitizeHelper::ClassMethods
- helper_method :format_date, :markdown
-
# By default, sets the charset to UTF-8 if it isn't already set
def set_charset
headers["Content-Type"] ||= "text/html; charset=UTF-8"
@@ -60,7 +40,7 @@ class ApplicationController < ActionController::Base
def set_session_expiration
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
unless session == nil
- return if @controller_name == 'feed' or session['noexpiry'] == "on"
+ return if self.controller_name == 'feed' or session['noexpiry'] == "on"
# If the method is called by the feed controller (which we don't have
# under session control) or if we checked the box to keep logged in on
# login don't set the session expiry time.
@@ -137,8 +117,8 @@ class ApplicationController < ActionController::Base
end
end
- def auto_complete_result2(entries, phrase = nil)
- json_elems = "[{" + entries.map {|item| "\"id\" : \"#{item.id}\", \"value\" : \"#{item.specification()}\""}.join("},{") + "}]"
+ def format_dependencies_as_json_for_auto_complete(entries)
+ json_elems = "[{" + entries.map {|item| "\"value\" : \"#{item.id}\", \"label\" : \"#{item.specification()}\""}.join("},{") + "}]"
return json_elems == "[{}]" ? "" : json_elems
end
@@ -150,7 +130,7 @@ class ApplicationController < ActionController::Base
def markdown(text)
RedCloth.new(text).to_html
end
-
+
# Here's the concept behind this "mobile content negotiation" hack: In
# addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
# 'mobile' version designed to be suitablef or use from a phone or PDA. It
diff --git a/app/controllers/backend_controller.rb b/app/controllers/backend_controller.rb
index 0a96cb30..5fceed53 100644
--- a/app/controllers/backend_controller.rb
+++ b/app/controllers/backend_controller.rb
@@ -1,3 +1,5 @@
+class CannotAccessContext < RuntimeError; end
+
class BackendController < ApplicationController
wsdl_service_name 'Backend'
web_service_api TodoApi
diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb
index 08ded1c9..c9644244 100644
--- a/app/controllers/login_controller.rb
+++ b/app/controllers/login_controller.rb
@@ -6,6 +6,9 @@ class LoginController < ApplicationController
skip_before_filter :login_required
before_filter :login_optional
before_filter :get_current_user
+
+ protect_from_forgery :except => :check_expiry
+
if ( SITE_CONFIG['authentication_schemes'].include? 'cas')
# This will allow the user to view the index page without authentication
# but will process CAS authentication data if the user already
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index fadf2db3..a1f8f6ba 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -47,10 +47,11 @@ class ProjectsController < ApplicationController
init_data_for_sidebar unless mobile?
@page_title = t('projects.page_title', :project => @project.name)
- @not_done = @project.todos.active_or_hidden
- @deferred = @project.deferred_todos
- @pending = @project.pending_todos
- @done = @project.todos.find_in_state(:all, :completed, :order => "todos.completed_at DESC", :limit => current_user.prefs.show_number_completed, :include => [:context])
+ @not_done = @project.todos.active_or_hidden(:include => [:tags, :context, :predecessors])
+ @deferred = @project.deferred_todos(:include => [:tags, :context, :predecessors])
+ @pending = @project.pending_todos(:include => [:tags, :context, :predecessors])
+ @done = @project.todos.find_in_state(:all, :completed,
+ :order => "todos.completed_at DESC", :limit => current_user.prefs.show_number_completed, :include => [:context, :tags, :predecessors])
@count = @not_done.size
@down_count = @count + @deferred.size + @pending.size
@@ -59,6 +60,7 @@ class ProjectsController < ApplicationController
@default_tags = @project.default_tags
@new_note = current_user.notes.new
@new_note.project_id = @project.id
+ @contexts = current_user.contexts
respond_to do |format|
format.html
format.m &render_project_mobile
@@ -237,9 +239,9 @@ class ProjectsController < ApplicationController
def render_projects_mobile
lambda do
- @active_projects = @projects.active
- @hidden_projects = @projects.hidden
- @completed_projects = @projects.completed
+ @active_projects = current_user.projects.active
+ @hidden_projects = current_user.projects.hidden
+ @completed_projects = current_user.projects.completed
@down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size
cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']}
render :action => 'index_mobile'
diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb
index 0d17e24b..126e7c4b 100644
--- a/app/controllers/recurring_todos_controller.rb
+++ b/app/controllers/recurring_todos_controller.rb
@@ -112,17 +112,17 @@ class RecurringTodosController < ApplicationController
end
if @saved
- @status_message = "The recurring todo was saved"
+ @status_message = t('todos.recurring_action_saved')
@todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false
if @todo_saved
- @status_message += " / created a new todo"
+ @status_message += " / " + t('todos.new_related_todo_created_short')
else
- @status_message += " / did not create todo"
+ @status_message += " / " + t('todos.new_related_todo_not_created_short')
end
@down_count = current_user.recurring_todos.active.count
@new_recurring_todo = RecurringTodo.new
else
- @status_message = "Error saving recurring todo"
+ @status_message = t('todos.error_saving_recurring')
end
respond_to do |format|
@@ -151,10 +151,10 @@ class RecurringTodosController < ApplicationController
format.html do
if @saved
- notify :notice, "Successfully deleted recurring action", 2.0
+ notify :notice, t('todos.recurring_deleted_success'), 2.0
redirect_to :action => 'index'
else
- notify :error, "Failed to delete the recurring action", 2.0
+ notify :error, t('todos.error_deleting_recurring', :description => @recurring_todo.description), 2.0
redirect_to :action => 'index'
end
end
@@ -251,11 +251,17 @@ class RecurringTodosController < ApplicationController
private
def init
- @days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]]
- @months_of_year = [
- ['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6],
- ['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]]
- @xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]]
+ @days_of_week = []
+ 0.upto 6 do |i|
+ @days_of_week << [t('date.day_names')[i], i]
+ end
+
+ @months_of_year = []
+ 1.upto 12 do |i|
+ @months_of_year << [t('date.month_names')[i], i]
+ end
+
+ @xth_day = [[t('common.first'),1],[t('common.second'),2],[t('common.third'),3],[t('common.fourth'),4],[t('common.last'),5]]
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
end
diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb
index 880c0de7..61892c55 100644
--- a/app/controllers/todos_controller.rb
+++ b/app/controllers/todos_controller.rb
@@ -11,6 +11,12 @@ class TodosController < ApplicationController
:completed_archive, :check_deferred, :toggle_check, :toggle_star,
:edit, :update, :defer, :create, :calendar, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor]
+ protect_from_forgery :except => :check_deferred
+
+ # these are needed for todo_feed_content. TODO: remove this view stuff from controller
+ include ActionView::Helpers::SanitizeHelper
+ extend ActionView::Helpers::SanitizeHelper::ClassMethods
+
def index
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
@@ -49,7 +55,7 @@ class TodosController < ApplicationController
def create
@source_view = params['_source_view'] || 'todo'
@default_context = current_user.contexts.find_by_name(params['default_context_name'])
- @default_project = current_user.projects.find_by_name(params['default_project_name'])
+ @default_project = current_user.projects.find_by_name(params['default_project_name']) unless params['default_project_name'].blank?
@tag_name = params['_tag_name']
@@ -248,6 +254,7 @@ class TodosController < ApplicationController
end
def remove_predecessor
+ puts "@@@ start remove_predecessor"
@source_view = params['_source_view'] || 'todo'
@todo = current_user.todos.find(params['id'])
@predecessor = current_user.todos.find(params['predecessor'])
@@ -440,7 +447,7 @@ class TodosController < ApplicationController
format.js do
if @saved
determine_down_count
- if source_view_is_one_of(:todo, :deferred)
+ if source_view_is_one_of(:todo, :deferred, :project)
determine_remaining_in_context_count(@context_id)
elsif source_view_is :calendar
@original_item_due_id = get_due_id_for_calendar(@original_item_due)
@@ -510,7 +517,7 @@ class TodosController < ApplicationController
def tag
init_data_for_sidebar unless mobile?
@source_view = params['_source_view'] || 'tag'
- @tag_name = params[:name]
+ @tag_name = sanitize(params[:name]) # sanitize to prevent XSS vunerability!
@page_title = t('todos.tagged_page_title', :tag_name => @tag_name)
# mobile tags are routed with :name ending on .m. So we need to chomp it
@@ -648,7 +655,7 @@ class TodosController < ApplicationController
get_todo_from_params
# Begin matching todos in current project
@items = current_user.todos.find(:all,
- :select => 'description, project_id, context_id, created_at',
+ :include => [:context, :project],
:conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND ' +
'NOT (id = ?) AND lower(description) LIKE ? AND project_id = ?',
'active', 'pending', 'deferred',
@@ -660,7 +667,7 @@ class TodosController < ApplicationController
)
if @items.empty? # Match todos in other projects
@items = current_user.todos.find(:all,
- :select => 'description, project_id, context_id, created_at',
+ :include => [:context, :project],
:conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND ' +
'NOT (id = ?) AND lower(description) LIKE ?',
'active', 'pending', 'deferred',
@@ -672,7 +679,7 @@ class TodosController < ApplicationController
else
# New todo - TODO: Filter on project
@items = current_user.todos.find(:all,
- :select => 'description, project_id, context_id, created_at',
+ :include => [:context, :project],
:conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND lower(description) LIKE ?',
'active', 'pending', 'deferred',
'%' + params[:term].downcase + '%' ],
@@ -680,7 +687,7 @@ class TodosController < ApplicationController
:limit => 10
)
end
- render :inline => auto_complete_result2(@items)
+ render :inline => format_dependencies_as_json_for_auto_complete(@items)
end
def convert_to_project
@@ -822,13 +829,13 @@ class TodosController < ApplicationController
# current_users.todos.find but that broke with_scope for :limit
# Exclude hidden projects from count on home page
- @todos = current_user.todos.find(:all, :include => [ :project, :context, :tags ])
+ @todos = current_user.todos.find(:all, :include => [ :project, :context, :tags, :pending_successors, :recurring_todo ])
# Exclude hidden projects from the home page
@not_done_todos = current_user.todos.find(:all,
:conditions => ['contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', false, 'active'],
:order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC",
- :include => [ :project, :context, :tags ])
+ :include => [ :project, :context, :tags, :pending_successors, :recurring_todo ])
end
end
@@ -891,12 +898,13 @@ class TodosController < ApplicationController
@remaining_hidden_count = current_user.todos.hidden.with_tag(tag).count
}
from.project {
- @remaining_deferred_or_pending_count = current_user.projects.find(@todo.project_id).todos.deferred_or_blocked.count
- @remaining_in_context = current_user.projects.find(@todo.project_id).todos.active.count
- @target_context_count = current_user.projects.find(@todo.project_id).todos.active.count
+ project_id = @project_changed ? @original_item_project_id : @todo.project_id
+ @remaining_deferred_or_pending_count = current_user.projects.find(project_id).todos.deferred_or_blocked.count
+ @remaining_in_context = current_user.projects.find(project_id).todos.active.count
+ @target_context_count = current_user.projects.find(project_id).todos.active.count
}
from.calendar {
- @target_context_count = count_old_due_empty(@new_due_id)
+ @target_context_count = @new_due_id.blank? ? 0 : count_old_due_empty(@new_due_id)
}
end
@remaining_in_context = current_user.contexts.find(context_id).todos(true).active.not_hidden.count if !@remaining_in_context
@@ -978,6 +986,7 @@ class TodosController < ApplicationController
end
def todo_feed_content
+ # TODO: move view stuff into view, also the includes at the top
lambda do |i|
item_notes = sanitize(markdown( i.notes )) if i.notes?
due = "
\n" if i.due?
@@ -1325,4 +1334,4 @@ class TodosController < ApplicationController
end
-end
\ No newline at end of file
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0f6fa52c..0af710a0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -240,7 +240,7 @@ module ApplicationHelper
end
def generate_i18n_strings
- js = ""
+ js = "i18n_locale='#{I18n.locale}';\n"
js << "i18n = new Array();\n"
%w{
shared.toggle_multi shared.toggle_multi_title
@@ -259,4 +259,12 @@ module ApplicationHelper
return js
end
+ def javascript_tag_for_i18n_datepicker
+ locale = I18n.locale
+ # do not include en as locale since this the available by default
+ if locale and locale != :en
+ javascript_include_tag("i18n/jquery.ui.datepicker-#{locale}.js")
+ end
+ end
+
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 7f7bbc4d..1f347fb4 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -11,6 +11,7 @@ module TodosHelper
image_tag("blank.png", :alt => t('todos.edit'), :align => "absmiddle", :id => 'edit_icon_todo_'+todo.id.to_s, :class => 'edit_item'),
{:controller => 'todos', :action => 'edit', :id => todo.id},
:class => "icon edit_item",
+ :id => "icon_edit_todo_#{todo.id}",
:title => t('todos.edit_action_with_description', :description => todo.description))
end
@@ -29,11 +30,13 @@ module TodosHelper
:_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")}
url[:_tag_name] = @tag_name if @source_view == 'tag'
- futuredate = (todo.show_from || todo.user.date) + days.days
options = {:x_defer_alert => false, :class => "icon_defer_item" }
- if todo.due && futuredate > todo.due
- options[:x_defer_alert] = true
- options[:x_defer_date_after_due_date] = t('todos.defer_date_after_due_date')
+ if todo.due
+ futuredate = (todo.show_from || todo.user.date) + days.days
+ if futuredate > todo.due
+ options[:x_defer_alert] = true
+ options[:x_defer_date_after_due_date] = t('todos.defer_date_after_due_date')
+ end
end
return link_to(image_tag_for_defer(days), url, options)
@@ -87,7 +90,7 @@ module TodosHelper
end
def remote_toggle_checkbox(todo=@todo)
- check_box_tag('item_id', toggle_check_todo_path(todo), todo.completed?, :class => 'item-checkbox',
+ check_box_tag("mark_complete_#{todo.id}", toggle_check_todo_path(todo), todo.completed?, :class => 'item-checkbox',
:title => todo.pending? ? t('todos.blocked_by', :predecessors => todo.uncompleted_predecessors.map(&:description).join(', ')) : "", :readonly => todo.pending?)
end
@@ -139,10 +142,6 @@ module TodosHelper
if tag_list.empty? then "" else "#{tag_list}" end
end
- def predecessor_list_text(todo=@todo)
- todo.predecessors.map{|t| t.specification}.join(', ')
- end
-
def deferred_due_date(todo=@todo)
if todo.deferred? && todo.due
t('todos.action_due_on', :date => format_date(todo.due))
@@ -225,7 +224,6 @@ module TodosHelper
page.todo { return !@todo.hidden? }
page.deferred { return @todo.deferred? || @todo.pending? }
page.context {
- logger.debug "ci=#{@todo.context_id} dci=#{@default_context.id} th=#{@todo.hidden?} tch=#{@todo.context.hidden?}"
return @todo.context_id==@default_context.id && ( (@todo.hidden? && @todo.context.hidden?) || (!@todo.hidden?) )
}
page.tag {
@@ -266,7 +264,7 @@ module TodosHelper
end
def default_contexts_for_autocomplete
- projects = current_user.projects.find(:all, :conditions => ['default_context_id is not null'])
+ projects = current_user.projects.find(:all, :include => [:context], :conditions => ['default_context_id is not null'])
Hash[*projects.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
end
@@ -289,7 +287,7 @@ module TodosHelper
end
def date_field_tag(name, id, value = nil, options = {})
- text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys)
+ text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "autocomplete" => "off"}.update(options.stringify_keys)
end
def update_needs_to_hide_context
@@ -297,7 +295,7 @@ module TodosHelper
(@remaining_in_context == 0 && @todo_was_deferred_from_active_state) ||
(@remaining_in_context == 0 && @todo.completed? && !(@original_item_was_deferred || @original_item_was_hidden)) if source_view_is(:tag)
- return false if source_view_is(:project)
+ return false if source_view_is_one_of(:project, :calendar)
return (@remaining_in_context == 0) && !source_view_is(:context)
end
@@ -305,7 +303,7 @@ module TodosHelper
def update_needs_to_remove_todo_from_container
source_view do |page|
page.context { return @context_changed || @todo.deferred? || @todo.pending? }
- page.project { return @todo_deferred_state_changed || @todo_pending_state_changed }
+ page.project { return @todo_deferred_state_changed || @todo_pending_state_changed || @project_changed}
page.deferred { return @context_changed || !(@todo.deferred? || @todo.pending?) }
page.calendar { return @due_date_changed || !@todo.due }
page.stats { return @todo.completed? }
@@ -379,8 +377,8 @@ module TodosHelper
container_id = "p#{@original_item_project_id}empty-nd" if @remaining_in_context == 0
container_id = "tickler-empty-nd" if (
( (@todo_was_activated_from_deferred_state || @todo_was_activated_from_pending_state) && @remaining_deferred_or_pending_count == 0) ||
- (@original_item_was_deferred && @remaining_deferred_or_pending_count == 0 && @todo.completed?) )
- container_id = "empty-d" if @completed_count && @completed_count == 0 && !@todo.completed?
+ (@original_item_was_deferred && @remaining_deferred_or_pending_count == 0 && @todo.completed?) )
+ container_id = "empty-d" if @completed_count && @completed_count == 0 && !@todo.completed?
}
page.deferred { container_id = "c#{@original_item_context_id}empty-nd" if @remaining_in_context == 0 }
page.calendar { container_id = "empty_#{@original_item_due_id}" if @old_due_empty }
@@ -405,7 +403,7 @@ module TodosHelper
end
end
html += "}}) " * animation.count
- return html
+ return html + ";"
end
private
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3456f55c..72ac6c0f 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -108,84 +108,41 @@ class Todo < ActiveRecord::Base
# Returns a string with description
def specification
- project_name = project.is_a?(NullProject) ? "(none)" : project.name
- return "\'#{description}\' <\'#{context.title}\'; \'#{project_name}\'>"
+ project_name = self.project.is_a?(NullProject) ? "(none)" : self.project.name
+ return "\'#{self.description}\' <\'#{self.context.title}\'; \'#{project_name}\'>"
end
-
- def todo_from_specification(specification)
- # Split specification into parts: description
- parts = specification.scan(RE_PARTS)
- return nil unless parts.length == 1
- return nil unless parts[0].length == 3
- todo_description = parts[0][0]
- context_name = parts[0][1]
- project_name = parts[0][2]
-
- # find the project
- project_id = nil;
- unless project_name == "(none)"
- project = Project.first(:conditions => {
- :user_id => self.user.id,
- :name => project_name
- })
- project_id = project.id unless project.nil?
- end
-
- todos = Todo.all(
- :joins => :context,
- :conditions => {
- :description => todo_description,
- :user_id => self.user.id,
- :contexts => {:name => context_name},
- :project_id => project_id
- }
- )
-
- return nil if todos.empty?
-
- # TODO: what todo if there are more than one todo that fit the specification
- return todos[0]
- end
-
+
def validate
if !show_from.blank? && show_from < user.date
errors.add("show_from", I18n.t('models.todo.error_date_must_be_future'))
end
- errors.add(:description, "may not contain \" characters") if /\"/.match(description)
+ errors.add(:description, "may not contain \" characters") if /\"/.match(self.description)
unless @predecessor_array.nil? # Only validate predecessors if they changed
- @predecessor_array.each do |specification|
- t = todo_from_specification(specification)
- if t.nil?
- errors.add("Depends on:", "Could not find action '#{h(specification)}'")
- else
- errors.add("Depends on:", "Adding '#{h(specification)}' would create a circular dependency") if is_successor?(t)
- end
+ @predecessor_array.each do |todo|
+ errors.add("Depends on:", "Adding '#{h(todo.specification)}' would create a circular dependency") if is_successor?(todo)
end
end
end
def save_predecessors
unless @predecessor_array.nil? # Only save predecessors if they changed
- current_array = predecessors.map{|p| p.specification}
+ current_array = self.predecessors
remove_array = current_array - @predecessor_array
add_array = @predecessor_array - current_array
@removed_predecessors = []
- # This is probably a bit naive code...
- remove_array.each do |specification|
- t = todo_from_specification(specification)
- unless t.nil?
- @removed_predecessors << t
- self.predecessors.delete(t)
+ remove_array.each do |todo|
+ unless todo.nil?
+ @removed_predecessors << todo
+ self.predecessors.delete(todo)
end
end
- # ... as is this?
- add_array.each do |specification|
- t = todo_from_specification(specification)
- unless t.nil?
- self.predecessors << t unless self.predecessors.include?(t)
+
+ add_array.each do |todo|
+ unless todo.nil?
+ self.predecessors << todo unless self.predecessors.include?(todo)
else
- logger.error "Could not find #{specification}" # Unexpected since validation passed
+ logger.error "Could not find #{todo.description}" # Unexpected since validation passed
end
end
end
@@ -196,20 +153,22 @@ class Todo < ActiveRecord::Base
end
def remove_predecessor(predecessor)
+ puts "@@@ before delete"
# remove predecessor and activate myself
- predecessors.delete(predecessor)
+ self.predecessors.delete(predecessor)
+ puts "@@@ before activate"
self.activate!
end
# Returns true if t is equal to self or a successor of self
- def is_successor?(t)
- if self == t
+ def is_successor?(todo)
+ if self == todo
return true
elsif self.successors.empty?
return false
else
self.successors.each do |item|
- if item.is_successor?(t)
+ if item.is_successor?(todo)
return true
end
end
@@ -314,13 +273,21 @@ class Todo < ActiveRecord::Base
def add_predecessor_list(predecessor_list)
return unless predecessor_list.kind_of? String
- @predecessor_array = predecessor_list.scan(RE_SPEC)
+
+ @predecessor_array=[]
+
+ predecessor_ids_array = predecessor_list.split(",")
+ predecessor_ids_array.each do |todo_id|
+ predecessor = self.user.todos.find_by_id( todo_id.to_i ) unless todo_id.blank?
+ @predecessor_array << predecessor unless predecessor.nil?
+ end
+
return @predecessor_array
end
def add_predecessor(t)
- @predecessor_array = predecessors.map{|p| p.specification}
- @predecessor_array << t.specification
+ @predecessor_array = predecessors
+ @predecessor_array << t
end
# Return todos that should be activated if the current todo is completed
diff --git a/app/views/integrations/index.de.html.erb b/app/views/integrations/index.de.html.erb
index 2eb83f5a..52f812e9 100644
--- a/app/views/integrations/index.de.html.erb
+++ b/app/views/integrations/index.de.html.erb
@@ -100,8 +100,12 @@
If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:
-
Go to <%= link_to "Preferences", preferences_url %> and set your "From email" and "default email context" for todos sent in via email (which could come from an SMS message)
-
In sendmail/qmail/postfix/whatever, set up an email address alias to pipe messages to
/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
+
Go to <%= link_to t('layouts.navigation.preferences'), preferences_url %>
+ and set your "<%= Preference.human_attribute_name('sms_email') %>" and
+ "<%= Preference.human_attribute_name('sms_context') %>" for todos sent in
+ via email (which could come from an SMS message)
+
In sendmail/qmail/postfix/whatever, set up an email address alias
+ to pipe messages to
/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
Send an email to your newly configured address!
You can also use the Rich Todo API to send in tasks like "do laundry @ Home"
diff --git a/app/views/integrations/index.en.html.erb b/app/views/integrations/index.en.html.erb
index aa6c88a1..2e2160c3 100644
--- a/app/views/integrations/index.en.html.erb
+++ b/app/views/integrations/index.en.html.erb
@@ -100,8 +100,12 @@
If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:
-
Go to <%= link_to "Preferences", preferences_url %> and set your "From email" and "default email context" for todos sent in via email (which could come from an SMS message)
-
In sendmail/qmail/postfix/whatever, set up an email address alias to pipe messages to
/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
+
Go to <%= link_to t('layouts.navigation.preferences'), preferences_url %> and
+ set your "<%= Preference.human_attribute_name('sms_email') %>" and
+ "<%= Preference.human_attribute_name('sms_context') %>" for todos sent in
+ via email (which could come from an SMS message)
+
In sendmail/qmail/postfix/whatever, set up an email address
+ alias to pipe messages to
/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
Send an email to your newly configured address!
You can also use the Rich Todo API to send in tasks like "do laundry @ Home"
diff --git a/app/views/integrations/index.nl.html.erb b/app/views/integrations/index.nl.html.erb
new file mode 100644
index 00000000..390a2040
--- /dev/null
+++ b/app/views/integrations/index.nl.html.erb
@@ -0,0 +1,134 @@
+<% has_contexts = !current_user.contexts.empty? -%>
+
Integratie
+
Tracks kan met een aantal tools worden geïntegreerd...
+ Alles om je te helpen om dingen gedaan te krijgen!
+ Deze pagina heeft informatie over het tot stand brengen van sommige integratievormen.
+ Deze voorbeelden zijn niet altijd voor alle platformen van toepassing en
+ sommige voorbeelden vragen meer technische kennis dan anderen
+ Zie ook <%= link_to "de documentatie voor ontwikkelaars met Tracks' REST API", url_for(:action => 'rest_api') %>.
Dit is een eenvoudig script die een dialog box toont om jou om een beschrijving te vragen en vervolgens die op te sturen naar Tracks
+ met een hard-coded context.
+
+<% if has_contexts -%>
+
+
Kies de context waar de je acties aan toe wilt laten voegen:
+
+
Kopieer de volgende Applescript naar het klembord.
+
+
+
Open de Script Editor en plak het script in een nieuw document.
+
Compileer en bewaar het script. Voert het uit wanneer nodig.
+
+<% else %>
+
Je hebt nog geen context(en). Het script komt beschikbaar als je het eerste context hebt toegevoegd.
+<% end %>
+
+
+
Voeg een acties toe met Applescript op basis van de huidig geselecteerde e-mail in Mail.app
+
Dit script neemt de verstuurder en het onderwerp van de geselecteerde email(s)
+ van Mail over en maakt een nieuwe acties voor elke email met de beschrijving
+ "Email [sender] about [subject]". De beschrijving wordt, als nodig, na 100 karakters afgebroken
+ (dit is de limiet voor een beschrijving). Het heeft ook Growl notificaties mocht je Growl geïnstalleerd hebben.
+
+<% if has_contexts -%>
+
+
Kies de context waar de je acties aan toe wilt laten voegen:
+
+
Kopieer de volgende Applescript naar het klembord.
+
+
+
Open de Script Editor en plak het script in een nieuw document.
+
Compileer en bewaar het script in de directory ~/Library/Scripts/Mail Scripts.
+
Voor meer informatie over het gebruiken van AppleScript met Mail.app, zie
+ dit overzicht.
+
+<% else %>
+
Je hebt nog geen context(en). Het script komt beschikbaar als je het eerste context hebt toegevoegd.
+<% end %>
+
+
+
Voeg acties toe met Quicksilver en Applescript
+
+
Dit integratievoorbeeld laat je acties toevoegen aan Tracks via Quicksilver.
+
+<% if has_contexts -%>
+
+
Kies de context waar de je acties aan toe wilt laten voegen:
+
+
Kopieer de volgende Applescript naar het klembord.
+
+
+
Open de Script Editor en plak het script in een nieuw document.
+
Compileer en bewaar het script als "Add to Tracks.scpt" in ~/Library/Application Support/Quicksilver/Actions/
+ (mogelijk moet je eerst de Actions directory aanmaken)
+
Herstart Quicksilver
+
Activeer Quicksilver (Standaard via Ctrl+Space)
+
Toets "." om quicksilver in text mode te brengen
+
Voer de gewenste beschrijving van de actie in.
+
Toets tab om naar de action pane te gaan.
+
Via typen of bladeren, kies de "Add to Tracks" actie.
+
+<% else %>
+
Je hebt nog geen context(en). Het script komt beschikbaar als je het eerste context hebt toegevoegd.
+<% end %>
+
+
+
Email jezelf automatisch de acties met een aflopende deadline
+
+
Als je de volgende regel toevoegd aan jouw crontab, dat ontvang je een e-mail op elke dag rond 05:00 met een lijst met acties waarvan de deadline afloopt binnen de komende 7 dagen.
+
+
+
+
Uiteraard kan je ook een andere <%= link_to 'text feed gebruiken die Tracks biedt', feeds_path %> -- bijvoorbeeld een email met een lijst van acties voor een specifiek project naar een groep collega's die werken aan dat project?
+
+
+
Integreer Tracks met een email server om een actie via email naar Tracks te sturen
+
+ Als Tracks draait op dezelfde server als jouw mailserver, dan kan je de geïntegreerde mail handler gebruiken van Tracks. Om dit in te stellen:
+
+
+
Ga naar <%= link_to t('layouts.navigation.preferences'), preferences_url %>
+ en stel in "<%= Preference.human_attribute_name('sms_email') %>" en
+ "<%= Preference.human_attribute_name('sms_context') %>" voor acties die
+ verzonden zijn via email (die bijv. komen via een SMS message)
+
In sendmail/qmail/postfix/whatever, stel een email address
+ alias in om berichten door te sturen naar
+
/PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
+
Verstuur een email naar het net geconfigureerde e-mail adres!
+
+
Je kan ook de Rich Todo API gebruiken om acties te maken zoals "do laundry @ Home"
+ of "Call Bill > project X". Het onderwerp van het bericht zal de bijschrijving van de actie vullen,
+ de context, en het project, terwijl de body van het bericht de notities van de actie zal vullen.
+
+
+
+
Voeg tracks toe als een Google Gmail gadget
+
+ Je kan nu ook jouw projects/actions beheren in Gmail met de Tracks Gmail Gadget.
+ Voeg Tracks Gmail gadget toe aan de sidebar van Gmail en volg jouw acties
+ of voeg een nieuwe actie toe zonder apart een nieuw browser tab/scherm te openen
+ voor Tracks. Om dit in te stellen:
+
+
+
Log bij Gmail in en kies Settings in de rechter-bovenkant van jouw Gmail pagina. In de Gmail setting pagina, kies de Labs tab
+
Zet de "Add any gadget by URL" feature aan. Deze kan je onderop de lijst vinden. Kies voor Enable and kies Save Changes.
+
Nu zie je dat de Gadgets tab is toegevoegd aan de Gmail Settings. Ga nu naar de Gadgets tab
+
Knip en plak de volgende link in de Add a gadget by its URL: en klik dan op de Add button:
+
<%= integrations_url + "/google_gadget" %>
+
\ No newline at end of file
diff --git a/app/views/layouts/login.html.erb b/app/views/layouts/login.html.erb
index 9e4da310..36ebb2fd 100644
--- a/app/views/layouts/login.html.erb
+++ b/app/views/layouts/login.html.erb
@@ -3,7 +3,7 @@
<%= stylesheet_link_tag "scaffold" %>
- <%= javascript_include_tag 'jquery-1.5.min', 'jquery.cookie' %>
+ <%= javascript_include_tag 'jquery-1.5.1.min', 'jquery.cookie' %>
<%= @page_title -%>
diff --git a/app/views/layouts/mobile.m.erb b/app/views/layouts/mobile.m.erb
index 42c1c81e..ab8ff179 100644
--- a/app/views/layouts/mobile.m.erb
+++ b/app/views/layouts/mobile.m.erb
@@ -12,8 +12,8 @@
<%= @page_title %>
<% if !(@new_mobile || @edit_mobile)
- if !current_user.prefs.nil? -%>
-
" class="list"><%-# list needs to be here for edit form to work -%>
<%= render :partial => "projects/project_settings", :object => project, :locals => { :collapsible => collapsible } %>
-
Actions in this project
+
<%= t('projects.actions_in_project_title') %>
-
Currently there are no incomplete actions in this project
This project
- <% if project.completed? -%>has been marked as completed
- <% elsif project.hidden? -%>has been marked as hidden
- <% else -%>is active
+
<%= t('projects.this_project') %>
+ <% if project.completed? -%><%= t('projects.was_marked_complete') %>
+ <% elsif project.hidden? -%><%= t('projects.was_marked_hidden') %>
+ <% else -%><%= t('projects.is_active') %>
<% end -%>
- with <% if project.default_context.nil? -%>
- no default context
+ <% if project.default_context.nil? -%>
+ <%= t('projects.with_no_default_context') %>
<% else -%>
- a default context of <%= project.default_context.name -%>
+ <%= t('projects.with_default_context', :context_name => project.default_context.name) %>
<% end -%>
- and with <% if project.default_tags.nil? || project.default_tags.blank? -%>
- no default tags.
+ <% if project.default_tags.nil? || project.default_tags.blank? -%>
+ <%= t('projects.with_no_default_tags') %>.
<% else -%>
- '<%= project.default_tags -%>' as the default tags.
+ <%= t('projects.with_default_tags', :tags => project.default_tags) %>.
<% end -%>
- <%= link_to_edit_project(project, "Edit Project Settings") %>
+ <%= link_to_edit_project(project, t('projects.edit_project_settings')) %>
<%= render :partial => "shared/empty",
:locals => { :message => t('projects.no_notes_attached')} %>
diff --git a/app/views/projects/update.js.erb b/app/views/projects/update.js.erb
index 0b45527b..03730bf0 100644
--- a/app/views/projects/update.js.erb
+++ b/app/views/projects/update.js.erb
@@ -40,7 +40,10 @@ function update_project_page() {
}
function remove_project_edit_form() {
- $('#<%=dom_id(@project, 'edit')%>').hide(500, function() {$('#<%=dom_id(@project, 'edit')%>').remove();} );
+<%-
+ # do not remove() edit form as this will remove the DIV that is needed to replace with the new form, so only empty the DIV
+-%>
+ $('#<%=dom_id(@project, 'edit')%>').hide(500, function() {$('#<%=dom_id(@project, 'edit')%>').html("");} );
}
function update_and_show_project_settings() {
diff --git a/app/views/recurring_todos/_edit_form.html.erb b/app/views/recurring_todos/_edit_form.html.erb
index 2d61fd5c..e3ce0cbc 100644
--- a/app/views/recurring_todos/_edit_form.html.erb
+++ b/app/views/recurring_todos/_edit_form.html.erb
@@ -11,101 +11,104 @@
diff --git a/app/views/todos/toggle_check.js.erb b/app/views/todos/toggle_check.js.erb
index fe277420..e90c0741 100644
--- a/app/views/todos/toggle_check.js.erb
+++ b/app/views/todos/toggle_check.js.erb
@@ -7,7 +7,7 @@
animation = []
animation << "remove_todo"
if @todo.completed?
- animation << "add_to_completed_container"
+ animation << "add_to_completed_container" unless source_view_is(:calendar)
animation << "add_new_recurring_todo"
animation << "activate_pending_todos"
animation << "remove_source_container"
@@ -15,14 +15,15 @@
animation << "add_todo_to_context"
animation << "block_predecessors"
end
- animation << "update_empty_tag_container" if source_view_is(:tag) -%>
+ animation << "update_empty_container" if source_view_is_one_of(:tag, :todo) -%>
<%= render_animation(animation) %>
TracksPages.set_page_badge(<%= @down_count %>);
<% end -%>
<% end -%>
function redirect_after_complete() {
- redirect_to("<%= project_path(@todo.project) -%>");
+ var path = "<%= @todo.project_id.nil? ? "/" : project_path(@todo.project) -%>";
+ redirect_to(path);
}
function remove_todo(next_steps) {
@@ -85,42 +86,43 @@ function add_new_recurring_todo(next_steps) {
<% end -%>
}
-function update_empty_tag_container(next_steps) {
+function update_empty_container(next_steps) {
<% if @down_count==0 -%>
- $('#no_todos_in_tag_view').slideDown(400, function(){ next_steps.go(); });
+ $('#no_todos_in_view').slideDown(400, function(){ next_steps.go(); });
<% else -%>
- $('#no_todos_in_tag_view').fadeOut(100, function(){ next_steps.go(); });
+ $('#no_todos_in_view').fadeOut(100, function(){ next_steps.go(); });
<% end -%>
}
<% if @new_recurring_todo # hide js if @new_recurring_todo is not there-%>
function highlight_updated_recurring_todo(next_steps) {
- highlight_todo('#<%= dom_id(@new_recurring_todo)%>');
+ TodoItems.highlight_todo('#<%= dom_id(@new_recurring_todo)%>');
next_steps.go();
}
<% end -%>
function highlight_updated_todo(next_steps) {
- highlight_todo('#<%= dom_id(@todo)%>');
+ TodoItems.highlight_todo('#<%= dom_id(@todo)%>');
next_steps.go();
}
-function highlight_todo(id) {
- $(id).effect('highlight', {}, 2000, function(){ });
-}
-
function activate_pending_todos(next_steps) {
<% # Activate pending todos that are successors of the completed
if @saved && @pending_to_activate
# do not render the js in case of an error or if no todos to activate
@pending_to_activate.each do |t|
+ html = escape_javascript(render(:partial => t, :locals => { :parent_container_type => parent_container_type }))
+ # only project and tag view have a deferred/blocked container
if source_view_is_one_of(:project,:tag) -%>
$('#<%= dom_id(t) %>').fadeOut(400, function() {
$('#<%= dom_id(t) %>').remove();
+ $('#<%= item_container_id(t) %>').append("<%= html %>");
+ <%= "$('#tickler-empty-nd').show();" if @remaining_deferred_or_pending_count==0 -%>
});
- <% end -%>
- $('#<%= item_container_id(t) %>').append("<%= escape_javascript(render(:partial => t, :locals => { :parent_container_type => parent_container_type }))%>");
- highlight_todo('#<%= dom_id(t)%>');
+ <% else -%>
+ $('#<%= item_container_id(t) %>').append("<%= html%>");
+ <% end -%>
+ TodoItems.highlight_todo('#<%= dom_id(t)%>');
<% end -%>
<% end -%>
next_steps.go();
@@ -135,7 +137,7 @@ function block_predecessors(next_steps) {
$('#<%= dom_id(t) %>').remove();
<% if source_view_is(:project) or source_view_is(:tag) # Insert it in deferred/pending block if existing -%>
$('#<%= item_container_id(t) %>').append("<%= escape_javascript(render(:partial => t, :locals => { :parent_container_type => parent_container_type }))%>");
- highlight_todo('#<%= dom_id(t)%>');
+ TodoItems.highlight_todo('#<%= dom_id(t)%>');
<% end -%>
});
<% end -%>
diff --git a/app/views/todos/update.js.erb b/app/views/todos/update.js.erb
index adb412b3..7d2e62ad 100644
--- a/app/views/todos/update.js.erb
+++ b/app/views/todos/update.js.erb
@@ -15,12 +15,12 @@
end
animation << "hide_context" if update_needs_to_hide_context
animation << "highlight_updated_todo"
- animation << "update_empty_tag_container" if source_view_is(:tag)
+ animation << "update_empty_container" if source_view_is_one_of(:tag, :todo)
animation << "update_predecessors"
%>
<%= render_animation(animation) %>
- TracksPages.page_notify('notice', '<%=@status_message%>', 5);
+ TracksPages.page_notify('notice', '<%=escape_javascript @status_message%>', 5);
TracksPages.set_page_badge(<%= @down_count %>);
<% end %>
@@ -71,11 +71,11 @@ function highlight_updated_todo(next_steps) {
next_steps.go();
}
-function update_empty_tag_container(next_steps) {
+function update_empty_container(next_steps) {
<% if @down_count==0 -%>
- $('#no_todos_in_tag_view').slideDown(400, function(){ next_steps.go(); });
+ $('#no_todos_in_view').slideDown(400, function(){ next_steps.go(); });
<% else -%>
- $('#no_todos_in_tag_view').fadeOut(100, function(){ next_steps.go(); });
+ $('#no_todos_in_view').fadeOut(100, function(){ next_steps.go(); });
<% end -%>
}
diff --git a/config/environment.rb b/config/environment.rb
index 69d77007..e7398a18 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -25,7 +25,7 @@ Rails::Initializer.run do |config|
config.gem "RedCloth"
config.gem "soap4r", :lib => false
config.gem 'datanoise-actionwebservice', :lib => 'actionwebservice'
- config.gem 'sanitize'
+ config.gem 'sanitize', :version => '~> 1.2.1'
config.gem 'rack', :version => '1.1.0'
config.gem 'will_paginate', :version => '~> 2.3.15'
config.gem 'has_many_polymorphs'
@@ -121,7 +121,7 @@ if ( SITE_CONFIG['authentication_schemes'].include? 'cas')
end
end
-tracks_version='1.8devel'
+tracks_version='2.0devel'
# comment out next two lines if you do not want (or can not) the date of the
# last git commit in the footer
info=`git log --pretty=format:"%ai" -1`
diff --git a/config/environments/test.rb b/config/environments/test.rb
index c9695e9f..049cffe7 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -41,5 +41,5 @@ config.gem "ZenTest", :lib => "zentest", :version => ">=4.0.0"
config.gem "hpricot"
config.gem "hoe"
config.gem 'webrat', :lib => false, :version => '>=0.7.0' unless File.directory?(File.join(Rails.root, 'vendor/plugins/webrat'))
-config.gem 'rspec-rails', :lib => false, :version => '<2.1.0' unless File.directory?(File.join(Rails.root, 'vendor/plugins/rspec-rails'))
+config.gem 'rspec-rails', :lib => false, :version => '~>1.3.3' unless File.directory?(File.join(Rails.root, 'vendor/plugins/rspec-rails'))
config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com"
diff --git a/config/locales/de.yml b/config/locales/de.yml
index deddbc10..10b5cb5c 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -135,10 +135,7 @@ de:
activerecord:
attributes:
project:
- name:
- blank: Das Projekt muss benannt sein
- taken: ist bereits vergeben
- too_long: Der Projekt-Name muss kürzer als 256 Zeichen sein
+ name: Name
default_tags: Standard Tags
default_context_name: Standard Kontext
description: Beschreibung
@@ -441,7 +438,7 @@ de:
added_dependency: "%{dependency} als Abhängigkeit hinzugefügt."
no_deferred_actions: Zur Zeit sind keine zurückgestellten Aktionen vorhanden.
recurrence_completed: Nach dieser wiederkehrenden Aktion, die du gerade abgeschlossen hast, folgt keine mehr. Die Wiederholung endet hiermit
- no_actions_found: Keine Aktionen gefunden
+ no_actions_found_title: Keine Aktionen gefunden
in_pending_state: und als ausstehend markiert
due: Fällig
action_marked_complete_error: Die Aktion '%{description}' wurde aufgrund eines Fehlers NICHT als %{completed} markiert.
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e270b331..e88710f1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -39,6 +39,7 @@ en:
logout: "Logout"
cancel: "Cancel"
ok: "Ok"
+ add: "Add"
project: "Project"
projects: "Projects"
context: "Context"
@@ -65,7 +66,22 @@ en:
by_task_count: "By number of tasks"
by_task_count_title: "Sort by number of tasks"
by_task_count_title_confirm: "Are you sure that you want to sort these projects by the number of tasks? This will replace the existing sort order."
- drag_handle: "DRAG"
+ drag_handle: DRAG
+ bugs: Bugs
+ forum: Forum
+ wiki: Wiki
+ email: Email
+ website: Website
+ contribute: Contribute
+ first: First
+ second: Second
+ third: Third
+ fourth: Fourth
+ last: Last
+ errors:
+ user_unauthorized: "401 Unauthorized: Only administrative users are allowed access to this function."
+ footer:
+ send_feedback: Send feedback on %{version}
contexts:
status_hidden: "Context is hidden"
status_active: "Context is active"
@@ -239,13 +255,24 @@ en:
deferred_actions_empty: "There are no deferred actions for this project"
completed_actions: "Completed actions for this project"
completed_actions_empty: "There are no completed actions for this project"
+ no_actions_in_project: "Currently there are no incomplete actions in this project"
+ actions_in_project_title: "Actions in this project"
notes: "Notes"
notes_empty: "There are no notes for this project"
settings: "Settings"
state: "This project is %{state}"
+ this_project: "This project"
active_projects: "Active projects"
hidden_projects: "Hidden projects"
completed_projects: "Completed projects"
+ was_marked_complete: "has been marked as completed"
+ was_marked_hidden: "has been marked as hidden"
+ is_active: "is active"
+ with_no_default_context: "with no default context"
+ with_default_context: "with a default context of '%{context_name}'"
+ with_no_default_tags: "and with no default tags"
+ with_default_tags: "and with '%{tags}' as the default tags"
+ edit_project_settings: "Edit Project Settings"
page_title: "TRACKS::Project: %{project}"
list_projects: "TRACKS::List Projects"
no_default_context: "This project does not have a default context"
@@ -397,9 +424,11 @@ en:
error_starring: "Could not toggle the star of this todo \'%{description}\'"
recurrence_completed: "There is no next action after the recurring action you just finished. The recurrence is completed"
tagged_with: "tagged with ‘%{tag_name}’"
- no_actions_found: "No actions found"
+ no_actions_found_title: "No actions found"
+ no_actions_found: "Currently there are no incomplete actions."
no_actions_with: "Currently there are no incomplete actions with the tag '%{tag_name}'"
removed_predecessor: "Removed %{successor} as dependency from %{predecessor}."
+ add_another_dependency: "Add another dependency"
error_removing_dependency: "There was an error removing the dependency"
deferred_actions_with: "Deferred actions with the tag '%{tag_name}'"
no_deferred_actions_with: "No deferred actions with the tag '%{tag_name}'"
@@ -407,6 +436,8 @@ en:
no_completed_actions_with: "No completed actions with the tag '%{tag_name}'"
next_action_description: "Next action description"
new_related_todo_created: "A new todo was added which belongs to this recurring todo"
+ new_related_todo_created_short: "created a new todo"
+ new_related_todo_not_created_short: "did not create todo"
error_completing_todo: "There was an error completing / activating the recurring todo %{description}"
recurring_todos: "Recurring todos"
no_recurring_todos: "Currently there are no recurring todos"
@@ -414,7 +445,9 @@ en:
no_completed_recurring: "Currently there are no completed recurring todos"
add_new_recurring: "Add a new recurring action"
recurring_deleted_success: "The recurring action was deleted succesfully."
+ deleted_success: "The action was deleted succesfully."
error_deleting_recurring: "There was an error deleting the recurring todo \'%{description}\'"
+ error_saving_recurring: "There was an error saving the recurring todo \'%{description}\'"
error_starring_recurring: "Could not toggle the star of recurring todo \'%{description}\'"
recurrence_period: "Recurrence period"
action_marked_complete: "The action '%{description}' was marked as %{completed}"
@@ -603,6 +636,4 @@ en:
openid_ok_pref_failed: "You have successfully verified %{url} as your identity but there was a problem saving your authentication preferences."
auth_type_updated: "Authentication type updated."
auth_type_update_error: "There was a problem updating your authentication type: %{error_messages}"
- new_token_generated: "New token successfully generated"
- errors:
- user_unauthorized: "401 Unauthorized: Only administrative users are allowed access to this function."
+ new_token_generated: "New token successfully generated"
\ No newline at end of file
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index c3d8d11f..e8ab0851 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -23,65 +23,54 @@ nl:
separator: ","
delimiter: .
- dates:
- month_names:
- - Januari
- - Februari
- - Maart
- - April
- - Mei
- - Juni
- - Juli
- - Augustus
- - September
- - Oktober
- - November
- - December
- day_names:
- - Zondag
- - Maandag
- - Dinsdag
- - Woensdag
- - Donderdag
- - Vrijdag
- - Zaterdag
integrations:
opensearch_description: Zoek in Tracks
gmail_description: Gadget om Tracks toe te voegen aan Gmail als een gadget
applescript_next_action_prompt: "Omschrijving van de actie:"
- applescript_success_after_id: gemaakt
applescript_success_before_id: Nieuwe actie met ID
+ applescript_success_after_id: gemaakt
common:
+ third: Derde
actions: Acties
back: Terug
add: Toevoegen
logout: Log uit
go_back: Ga terug
+ second: Tweede
none: Geen
cancel: Annuleer
optional: optioneel
notes: Notities
server_error: Een fout heeft op de server plaatsgevonden
+ forum: Forum
+ last: Laatste
action: Actie
projects: Projecten
project: Project
ok: Ok
+ contribute: Bijdragen
+ first: Eerste
+ website: Website
numbered_step: Stap %{number}
+ fourth: Vierde
context: Context
errors_with_fields: Er waren problemen met de volgende velden
+ drag_handle: SLEEP
sort:
by_task_count_title: Sorteer op aantal acties
by_task_count_title_confirm: Weet u zeker dat u deze projecten alphabetisch wilt sorteren? Dat zal de huidige sorteervolgorde aanpassen.
alphabetically: Alphabetisch
- sort: Sorteer
alphabetically_title: Sorteer projecten alphabetisch
+ sort: Sorteer
alphabetically_confirm: Weet u zeker dat u deze projecten alphabetisch wilt sorteren? Dat zal de huidige sorteervolgorde aanpassen.
by_task_count: Bij aantal acties
- drag_handle: SLEEP
create: Maken
description: Beschrijving
contexts: Contexten
update: Bijwerken
+ wiki: Wiki
+ bugs: Fouten
+ email: E-mail
ajaxError: Er is een fout opgetreden bij het ophalen van gegevens van de server
search: Zoeken
layouts:
@@ -98,13 +87,13 @@ nl:
contexts: 2-Contexten
home: 1-Start
navigation:
- recurring_todos: Terugkerende todos
manage_users_title: Toevoegen of verwijderen gebruikers
+ recurring_todos: Terugkerende acties
api_docs: REST API Docs
feeds: Feeds
- notes_title: Toon alle notities
stats: Statistieken
starred: Ster
+ notes_title: Toon alle notities
tickler_title: Tickler
manage_users: Beheren gebruikers
export_title: Import en export van gegevens
@@ -112,12 +101,12 @@ nl:
preferences: Voorkeuren
calendar_title: Kalender met acties met deadline
feeds_title: Zie een lijst met beschikbare feeds
- stats_title: Zie je statistieken
home_title: Start
starred_title: Zie je ster acties
recurring_todos_title: Beheren terugkerende acties
tickler: Tickler
completed_tasks: Gereed
+ stats_title: Zie je statistieken
view: Bekijk
organize: Organiseer
completed_tasks_title: Afgerond
@@ -134,55 +123,52 @@ nl:
feed_description: Een overzicht van alle projecten voor %{username}
todo:
error_date_must_be_future: moet een datum in de toekomst zijn
+ preference:
+ due_styles:
+ - Deadline over ____ dagen
+ - Deadline op ____
user:
error_context_not_associated: Context %{context} niet geassocieerd met gebruikers %{user}.
error_project_not_associated: Project %{project} niet geassocieerd met gebruikers %{user}.
- preference:
- due_styles:
- - "Deadline over ____ dagen"
- - "Deadline op ____"
data:
import_successful: De import was succesvol
import_errors: Er hebben zich fouten voorgedaan bij de import
activerecord:
attributes:
project:
- name:
- blank: Het project moet moet een naam hebben
- taken: bestaat al
- too_long: De naam van het project moet korter zijn dan 256 tekens
+ name: Naam
default_tags: Standaard Tags
default_context_name: Standaard context
description: Beschrijving
todo:
show_from: Tonen vanaf
- predecessors: Is afhankelijk van
+ predecessors: Afhankelijkheden
notes: Notities
project: Project
context: Context
description: Beschrijving
due: Deadline
+ preference:
+ show_hidden_projects_in_sidebar: Toon verborgen projecten in sidebar
+ show_hidden_contexts_in_sidebar: Toon verborgen contexten in sidebar
+ date_format: Datum formaat
+ staleness_starts: Begin van markeren openstaande actie
+ sms_context: Standaard context voor email
+ verbose_action_descriptors: Context en project uitschrijven in actielijst
+ mobile_todos_per_page: Acties per pagina (mobiel)
+ show_number_completed: Aantal te tonen afgeronde acties
+ title_date_format: Datum formaat in titel
+ refresh: Ververs interval (in minuten)
+ week_starts: Week start op
+ time_zone: Tijdzone
+ due_style: Deadline stijl
+ locale: Taal
+ sms_email: Van email
+ show_project_on_todo_done: Ga naar project pagina wanneer actie gereed is
+ show_completed_projects_in_sidebar: Toon afgeronde projecten in sidebar
user:
last_name: Achternaam
first_name: Voornaam
- preference:
- sms_context: Standaard context voor email
- mobile_todos_per_page: Acties per pagina (mobiel)
- refresh: Ververs interval (in minuten)
- week_starts: Week start op
- show_project_on_todo_done: Ga naar project pagina wanneer actie gereed is
- sms_email: Van email
- time_zone: Tijdzone
- due_style: Deadline stijl
- show_completed_projects_in_sidebar: Toon afgeronde projecten in sidebar
- show_hidden_projects_in_sidebar: Toon verborgen projecten in sidebar
- show_hidden_contexts_in_sidebar: Toon verborgen contexten in sidebar
- staleness_starts: Begin van markeren openstaande actie
- show_number_completed: Aantal te tonen afgeronde acties
- verbose_action_descriptors: Context en project uitschrijven in actielijst
- locale: Taal
- date_format: Datum formaat
- title_date_format: Datum formaat in titel
errors:
messages:
greater_than_or_equal_to: moet groter of gelijk zijn aan %{count}
@@ -216,7 +202,7 @@ nl:
body: Er waren problemen met de volgende velden
header:
one: 1 fout voorkomt het kunnen bewaren van deze %{model}
- other: "%{count} fout voorkomen dat dit %{model} bewaard kan worden"
+ other: "%{count} fouten voorkomen dat dit %{model} bewaard kan worden"
full_messages:
format: "%{attribute} %{message}"
stats:
@@ -237,7 +223,6 @@ nl:
weeks: Looptijd van een actie (weken). Klik op een balk voor meer info
totals_action_count: u heeft een totaal van %{count} acties
totals_deferred_actions: waarvan %{count} uitgestelde acties in de tickler zijn
- top10_longrunning: Top 10 langstlopende projecten
legend:
number_of_days: Aantal dagen geleden
actions: Acties
@@ -246,12 +231,13 @@ nl:
running_time: Looptijd van een actie (weken)
percentage: Percentage
months_ago: Maanden geleden
+ top10_longrunning: Top 10 langstlopende projecten
+ top5_contexts: Top 5 contexten
actions_lastyear_title: Acties in de afgelopen 12 maanden
totals_actions_completed: "%{count} van deze zijn voltooid."
totals_incomplete_actions: U heeft %{count} onvolledige acties
totals_unique_tags: Van deze tags zijn %{count} uniek.
actions_avg_completed_30days: en voltooide een gemiddelde van %{count} acties per dag.
- top5_contexts: Top 5 contexten
totals_visible_context_count: Van deze zijn %{count} zichtbare contexten
totals_blocked_actions: "%{count} zijn afhankelijk van de voltooiing van hun acties."
action_completion_time_title: Doorlooptijd (alle voltooide acties)
@@ -263,6 +249,7 @@ nl:
totals_project_count: U heeft %{count} projecten.
tags: Tags
actions_min_max_completion_days: De max-/minimum dagen tot voltooiing is %{min}/%{max}.
+ tag_cloud_90days_description: Deze tag cloud bevat tags van acties die zijn gemaakt of voltooid in de afgelopen 90 dagen.
running_time_all: Huidige looptijd van alle onvolledige acties
totals_tag_count: U heeft %{count} tags geplaatst op acties.
time_of_day: Tijd van de dag (alle acties)
@@ -272,26 +259,24 @@ nl:
top5_visible_contexts_with_incomplete_actions: Top 5 zichtbare contexten met onvolledige acties
actions_further: en verder
tod30: Tijd van de dag (laatste 30 dagen)
- tag_cloud_90days_description: Deze tag cloud bevat tags van acties die zijn gemaakt of voltooid in de afgelopen 90 dagen.
+ totals_completed_project_count: en %{count} zijn afgeronde projecten.
click_to_return: Klik %{link} om terug te keren naar de statistieken pagina.
top10_projects_30days: Top 10 project in de laatste 30 dagen
actions_selected_from_week: Gekozen acties van week
top10_projects: Top 10 projecten
spread_of_running_actions_for_visible_contexts: Verdeling van actieve acties voor zichtbare contexten
spread_of_actions_for_all_context: Verdeling van acties voor alle contexten
- actions_avg_created: In de afgelopen 12 maanden heeft u gemiddeld% {count} acties aangemaakt
+ actions_avg_created: In de afgelopen 12 maanden heeft u gemiddeld%{count} acties aangemaakt
click_to_show_actions_from_week: Klik %{link} om de acties van week %{week} en verder te zien.
other_actions_label: (anderen)
- totals_completed_project_count: en %{count} zijn afgeronde projecten.
+ totals: Totalen
time_of_day_legend:
number_of_actions: Aantal acties
time_of_day: Tijd van de dag
click_to_return_link: hier
totals_hidden_context_count: en %{count} zijn verborgen contexten.
actions_avg_completed: en voltooide een gemiddelde van %{count} acties per maand.
- totals: Totalen
no_actions_selected: Er zijn geen acties geselecteerd.
- click_to_update_actions: Klik op een balk in de grafiek op de acties hieronder aan te passen.
labels:
month_avg_completed: "%{months} gem afgerond per maand"
completed: Afgerond
@@ -299,45 +284,47 @@ nl:
avg_created: Gem gemaakt
avg_completed: Gem afgerond
created: Gemaakt
+ click_to_update_actions: Klik op een balk in de grafiek op de acties hieronder aan te passen.
running_time_all_legend:
actions: Acties
running_time: Looptijd van een actie (weken). Klik op een balk voor meer info
percentage: Percentage
- actions_actions_avg_created_30days: In de afgelopen 30 dagen heeft u gemiddeld% {count} acties gemaakt
+ action_selection_title: "TRACKS:: Actie selectie"
+ actions_actions_avg_created_30days: In de afgelopen 30 dagen heeft u gemiddeld %{count} acties gemaakt
tod30_legend:
number_of_actions: Aantal acties
time_of_day: Tijd van de dag
- action_selection_title: "TRACKS:: Actie selectie"
todos:
+ recurring_action_deleted: Actie werd verwijderd. Omdat deze actie herhalend is. werd een nieuwe actie toegevoegd
show_from: Toon vanaf
error_starring_recurring: Kon niet de ster van deze terugkerende actie niet omgezetten \'%{description}\'
- recurring_action_deleted: Actie werd verwijderd. Omdat deze actie herhalend is. werd een nieuwe actie toegevoegd
completed_actions: Voltooide acties
- completed_recurring: Afgesloten terugkerende todos
added_new_next_action: Nieuwe actie toegevoegd
+ completed_recurring: Afgesloten terugkerende todos
blocked_by: Geblokkeerd door %{predecessors}
star_action: Markeer deze actie met een ster
completed_recurrence_completed: Er is geen actie na de terugkerende actie die u new verwijderd heeft. De herhaling is voltooid
- defer_date_after_due_date: Uitsteldatum is na de vervaldag. Gelieve vervaldag bewerken alvorens uitstedatuml aan te passen.
+ defer_date_after_due_date: Uitsteldatum is na de vervaldag. Gelieve vervaldag bewerken alvorens uitsteldatum aan te passen.
unable_to_add_dependency: Niet in staat om de afhankelijkheid toe te voegen
done: Voltooid?
- star_action_with_description: markeer de actie'%{description}' met een ster
+ star_action_with_description: markeer de actie '%{description}' met een ster
tagged_with: gelabeld met ‘%{tag_name}’
completed: Afgerond
no_deferred_actions_with: Geen uitgestelde acties met de tag '%{tag_name}'
no_hidden_actions: Momenteel zijn er geen verborgen acties gevonden
- action_due_on: (deadline actie op %{date})
edit_action_with_description: Bewerk de actie '%{description}'
+ action_due_on: (deadline actie op %{date})
archived_tasks_title: "TRACKS:: Gearchiveerde voltooide taken"
remove_dependency: Verwijder afhankelijkheid (zal niet de actie zelf verwijderen)
list_incomplete_next_actions: Toon onvoltooide acties
- action_deleted_success: Actie succesvol verwijderd
tags: Tags (gescheiden door komma's)
- new_related_todo_created: Een nieuwe actie is toegevoegd, die behoort bij deze terugkerende todo
- context_changed: Context veranderd in %{name}
+ action_deleted_success: Actie succesvol verwijderd
mobile_todos_page_title: Alle acties
+ new_related_todo_created: Een nieuwe actie is toegevoegd, die behoort bij deze terugkerende todo
+ context_changed: Context veranderd in '%{name}'
+ add_another_dependency: Nog een afhankelijkheid toevoegen
delete_recurring_action_title: Verwijder de terugkerende actie
- removed_predecessor: "%{succesor} is verwijderd als afhankelijkheid van %{predecessor}."
+ removed_predecessor: "'%{successor}' is verwijderd als afhankelijkheid van '%{predecessor}'."
recurring_actions_title: TRACKS::Terugkerende acties
next_action_needed: U dient ten minste een actie in te vullen
action_saved: Actie opgeslagen
@@ -346,35 +333,38 @@ nl:
edit_action: Actie bewerken
added_new_context: Nieuwe context toegevoegd
next_actions_description: "Filter:"
+ older_completed_items: Oudere voltooide items
list_incomplete_next_actions_with_limit: Toont de laatste %{count} onvoltooide acties
- set_to_pending: "%{task} als wachtend ingesteld"
+ set_to_pending: "'%{task}' als wachtend ingesteld"
added_new_project: Nieuw project toegevoegd
next_actions_title_additions:
completed: acties voltooid
due_today: deadline vandaag
due_within_a_week: deadline binnen een week
- older_completed_items: Oudere voltooide items
+ task_list_title: TRACKS::Toon acties
append_in_this_project: in dit project
error_deleting_item: Er is een fout opgetreden bij het verwijderen van het item '%{description}'
- task_list_title: TRACKS::Toon acties
no_actions_due_this_week: Geen acties met deadline in rest van deze week
no_recurring_todos: Momenteel zijn er geen terugkerende acties
error_completing_todo: Er was een fout bij het voltooien / activeren van de terugkerende actie '%{description}'
convert_to_project: Maak project
- no_deferred_pending_actions: Momenteel zijn er geen uitgestelde of in wachtende acties
- completed_last_day: Voltooid in de laatste 24 uur
+ no_deferred_pending_actions: Momenteel zijn er geen uitgestelde of wachtende acties
delete_recurring_action_confirm: Weet u zeker dat u wilt de terugkerende actie '%{description}' wilt verwijderen?
- show_in_days: Toon over %{days} dagen
+ completed_last_day: Voltooid in de laatste 24 uur
+ error_saving_recurring: Er is een fout opgetreden het opslaan van de terugkerende actie '%{description}'
no_project: -- Geen project --
+ show_in_days: Toon over %{days} dagen
completed_more_than_x_days_ago: Voltooid meer dan %{count} dagen geleden
+ new_related_todo_created_short: een nieuwe actie gemaakt
feed_title_in_context: in context '%{context}'
older_than_days: Ouder dan %{count} dagen
edit: Bewerken
pending: Wachtend
completed_actions_with: Afgeronde acties met de tag %{tag_name}
- clear_due_date: Maak deadline leeg
+ deleted_success: De actie werd met succes verwijderd.
completed_tasks_title: TRACKS::Voltooide taken
feed_title_in_project: In het project '%{project}'
+ clear_due_date: Maak deadline leeg
error_removing_dependency: Er is een fout opgetreden het verwijderen van de afhankelijke actie
hidden_actions: Verborgen acties
was_due_on_date: had deadline op %{date}
@@ -391,14 +381,15 @@ nl:
calendar_page_title: TRACKS::Agenda
in_hidden_state: in verborgen toestand
show_today: Toon vandaag
+ no_actions_found_title: Geen acties gevonden
completed_last_x_days: Voltooid in de laatste %{count} dagen
- no_actions_with: Momenteel zijn er geen onvoltooide acties met de tag '%{tag_name}'
defer_x_days:
one: Een dag uitstellen
other: "%{count} dagen uitstellen"
+ no_actions_with: Momenteel zijn er geen onvoltooide acties met de tag '%{tag_name}'
added_new_next_action_singular: Nieuwe actie toegevoegd
no_completed_actions: Momenteel zijn er geen voltooide acties.
- deferred_pending_actions: Uitgestelde/wachtende van acties
+ deferred_pending_actions: Uitgestelde/wachtende acties
has_x_pending:
one: Heeft een wachtende actie
other: Heeft %{count} wachtende acties
@@ -419,6 +410,7 @@ nl:
one: U heeft een actie tot nu toe vandaag voltooid.
other: U heeft %{count} acties tot nu toe vandaag voltooid.
added_new_next_action_plural: Nieuwe acties toegevoegd
+ new_related_todo_not_created_short: een nieuwe actie is niet gemaakt
error_starring: Kon niet de ster van deze actie niet omzetten \'%{description}\'
show_tomorrow: Toon morgen
calendar:
@@ -433,9 +425,9 @@ nl:
due_this_month: Deadline in rest van %{month}
no_actions_due_this_month: Geen acties met deadline in de rest van deze maand
recurrence:
- ends_on_number_times: Eindigt na %{number} keer
ends_on_date: Eindigt op %{date}
every_work_day: Elke werkdag
+ ends_on_number_times: Eindigt na %{number} keer
recurrence_on_due_date: de datum dat deadline van de actie is
weekly_options: Instellingen voor de wekelijkse terugkerende acties
monthly_options: Instellingen voor maandelijks terugkerende acties
@@ -443,43 +435,43 @@ nl:
monthly: Maandelijks
starts_on: Begint op
daily_options: Instellingen voor dagelijks terugkerende acties
- daily: Dagelijks
show_option_always: altijd
+ daily: Dagelijks
yearly_every_x_day: Elke %{month} %{day}
recurrence_on_options: Stel herhaling in op
daily_every_number_day: Elke %{number} dag(en)
- ends_on: Eindigt op
weekly_every_number_week: Herhaalt elke %{number} weken op
+ ends_on: Eindigt op
show_options: Toon de actie
yearly_options: Instellingen voor jaarlijks terugkerende acties
- yearly_every_xth_day: De %{day} %{day_of_week} van %{month}
show_days_before: "%{days} dagen v\xC3\xB3\xC3\xB3r de deadline van actie"
+ yearly_every_xth_day: De %{day} %{day_of_week} van %{month}
from_tickler: de datum dat de actie uit de tickler komt (geen deadline ingesteld)
no_end_date: Geen einddatum
- day_x_on_every_x_month: Dag %{dag} op elke %{month} maand
+ day_x_on_every_x_month: Dag %{day} op elke %{month} maand
yearly: Jaarlijks
- monthly_every_xth_day: De %{dag} %{day_of_week} van elke %{month} maand
+ monthly_every_xth_day: De %{day} %{day_of_week} van elke %{month} maand
tagged_page_title: TRACKS::Tagged met '%{tag_name}'
no_completed_recurring: Momenteel zijn er geen voltooide terugkerende acties
added_dependency: "%{dependency} als afhankelijkheid toegevoegd."
no_deferred_actions: Momenteel zijn er geen uitgestelde acties.
recurrence_completed: Er is geen volgende actie na de terugkerende actie die u zojuist hebt voltooid. De herhaling is voltooid
- no_actions_found: Geen acties gevonden
+ no_actions_found: Momenteel zijn er geen onafgeronde acties.
in_pending_state: in wachtende toestand
due: Deadline
action_marked_complete_error: De actie '%{description}' is niet gemarkeerd als %{completed} vanwege een fout op de server.
- add_new_recurring: Voeg een nieuwe terugkerende actie toe
+ next_actions_description_additions:
+ completed: in de afgelopen %{count} dagen
+ due_date: met een deadline %{due_date} of eerder
depends_on_separate_with_commas: Afhankelijk van (gescheiden door komma's)
- recurring_action_saved: Terugkerende actie opgeslagen
action_saved_to_tickler: Actie opgeslagen in tickler
+ recurring_action_saved: Terugkerende actie opgeslagen
completed_in_archive:
one: Er is een voltooide actie in het archief.
other: Er zijn %{count} afgeronde acties in het archief.
to_tickler: naar tickler
- next_actions_description_additions:
- completed: in de afgelopen %{count} dagen
- due_date: met een deadline %{due_date} of eerder
overdue: Achterstallig
+ add_new_recurring: Voeg een nieuwe terugkerende actie toe
no_incomplete_actions: Er zijn geen onvoltooide acties
notes:
delete_note_title: Verwijder de notitie '%{id}'
@@ -490,8 +482,8 @@ nl:
show_note_title: Toon notitie
note_location_link: "In:"
edit_item_title: Item bewerken
- no_notes_available: "Momenteel zijn er geen notities: voeg notities toe aan projecten vanaf de individuele project pagina's."
note_header: Notitie %{id}
+ no_notes_available: "Momenteel zijn er geen notities: voeg notities toe aan projecten vanaf de individuele project pagina's."
delete_note_confirm: Weet u zeker dat u de notitie '%{id}' wilt verwijderen?
errors:
user_unauthorized: "401 Unauthorized: Alleen administratieve gebruikers mogen deze functie gebruiken."
@@ -513,8 +505,8 @@ nl:
long: "%A, %d. %B %Y, %H:%M"
pm: middag
preferences:
- open_id_url: Uw OpenID URL is
staleness_starts_after: Ophopen begint na %{days} dagen
+ open_id_url: Uw OpenID URL is
change_identity_url: Verander uw Identity URL
page_title: "TRACKS:: Voorkeuren"
change_password: Wijzig uw wachtwoord
@@ -526,54 +518,65 @@ nl:
generate_new_token: Genereer een nieuwe token
sms_context_none: Geen
token_header: Uw token
+ change_authentication_type: Verander uw authenticatietype
authentication_header: Uw authenticatie
current_authentication_type: Uw authenticatietype is %{auth_type}
- change_authentication_type: Verander uw authenticatietype
generate_new_token_confirm: Weet u dit zeker? Het genereren van een nieuw token zal de bestaande te vervangen en dit zal het extern gebruiken van de oude token laten mislukken.
projects:
+ no_actions_in_project: Momenteel zijn er geen onafgeronde acties in dit project
+ default_tags_removed_notice: De standaard tags zijn verwijderd
+ was_marked_hidden: is gemarkeerd als verborgen
default_context_set: Stel project standaard context in op %{default_context}
deferred_actions: Uitgestelde acties voor dit project
edit_project_title: Bewerk project
- default_tags_removed_notice: De standaard tags zijn verwijderd
page_title: "TRACKS:: Project: %{project}"
hide_form: Verberg formulier
deferred_actions_empty: Er zijn geen uitgestelde acties voor dit project
project_state: Project is %{state}.
show_form_title: Maak een nieuw project
to_new_project_page: Ga naar de nieuwe projectpagina
+ this_project: Dit project
no_notes_attached: Momenteel zijn er geen notities toegevoegd aan dit project
notes: Notities
todos_append: in dit project
notes_empty: Er zijn geen notities voor dit project
no_projects: Momenteel zijn er geen projecten
hide_form_title: Verberg nieuw project formulier
- completed_actions_empty: Er zijn nog geen afgeronde acties voor dit project
delete_project: Project verwijderen
+ with_no_default_context: zonder standaard context
+ completed_actions_empty: Er zijn nog geen afgeronde acties voor dit project
+ actions_in_project_title: Acties in dit project
+ with_default_context: met een standaard context '%{context_name}'
show_form: Toevoegen van een project
- delete_project_confirmation: Weet u zeker dat u wilt het project '%{naam} wilt verwijderen?
+ delete_project_confirmation: Weet u zeker dat u wilt het project '%{name} wilt verwijderen?
+ with_default_tags: en met '%{tags}' als de standaard tags
set_default_tags_notice: Stel project standaard tags in op %{default_tags}
add_note: Een notitie toevoegen
project_saved_status: Project opgeslagen
settings: Instellingen
completed_projects: Voltooide projecten
- list_projects: "TRACKS:: Overizcht van projecten"
+ list_projects: "TRACKS:: Overzicht van projecten"
+ is_active: is actief
add_project: Voeg project toe
- hidden_projects: Verborgen projecten
delete_project_title: Verwijder het project
+ hidden_projects: Verborgen projecten
default_context_removed: Standaard context verwijderd
completed_actions: Afgeronde acties voor dit project
add_note_submit: Notitie toevoegen
+ was_marked_complete: is gemarkeerd als voltooid
+ default_context: De standaard context voor dit project is %{context}
status_project_name_changed: Naam van het project werd gewijzigd
no_default_context: Dit project heeft geen standaard context
+ with_no_default_tags: en zonder standaard tags
+ edit_project_settings: Bewerk project instellingen
active_projects: Actieve projecten
state: Dit project is %{state}
- default_context: De standaard context voor dit project is %{context}
- date:
- month_names:
- -
+ date:
+ month_names:
+ -
- Januari
- Februari
- - Maar
+ - Maart
- April
- Mei
- Juni
@@ -583,11 +586,7 @@ nl:
- Oktober
- November
- December
- order:
- - :day
- - :month
- - :year
- abbr_day_names:
+ abbr_day_names:
- Zo
- Ma
- Di
@@ -595,12 +594,16 @@ nl:
- Do
- Vr
- Za
- formats:
+ order:
+ - :day
+ - :month
+ - :year
+ formats:
only_day: "%e"
default: "%d-%m-%Y"
short: "%e %b"
long: "%e %B %Y"
- day_names:
+ day_names:
- Zondag
- Maandag
- Dinsdag
@@ -608,8 +611,8 @@ nl:
- Donderdag
- Vrijdag
- Zaterdag
- abbr_month_names:
- -
+ abbr_month_names:
+ -
- Jan
- Feb
- Maa
@@ -629,12 +632,14 @@ nl:
two_words_connector: en
select:
prompt: Selecteer
+ footer:
+ send_feedback: Stuur reactie op %{version}
shared:
multiple_next_actions: Meerdere acties (een op elke regel)
toggle_single: Voeg een actie toe
hide_form: Verberg formulier
- add_actions: Toevoegen acties
add_action: Actie toevoegen
+ add_actions: Toevoegen acties
tags_for_all_actions: Tags voor alle acties (scheiden met een komma)
toggle_multi: Voeg meerdere acties toe
toggle_single_title: Voeg een nieuwe actie toe
@@ -643,14 +648,43 @@ nl:
separate_tags_with_commas: gescheiden door komma's
toggle_multi_title: Toggle single / multi actie formulier
hide_action_form_title: Verberg nieuwe actie formulier
+ dates:
+ month_names:
+ - Januari
+ - Februari
+ - Maart
+ - April
+ - Mei
+ - Juni
+ - Juli
+ - Augustus
+ - September
+ - Oktober
+ - November
+ - December
+ day_names:
+ - Zondag
+ - Maandag
+ - Dinsdag
+ - Woensdag
+ - Donderdag
+ - Vrijdag
+ - Zaterdag
+ sidebar:
+ list_name_active_contexts: Actieve contexten
+ list_name_active_projects: Actieve projecten
+ list_empty: Geen
+ list_name_completed_projects: Voltooide projecten
+ list_name_hidden_projects: Verborgen projecten
+ list_name_hidden_contexts: Verborgen contexten
users:
+ openid_url_verified: Je hebt %{url} met succes geverifieerd als je identiteit en uw authenticatie type OpenID opgeslagen.
+ destroy_successful: Gebruiker %{login} met succes verwijderd
auth_type_update_error: "Er was een probleem met het bijwerken van uw authenticatietype: %{error_messages}"
total_contexts: Totaal aantal contexten
first_user_heading: "Welkom bij TRACKS. Om te beginnen, maak dan een admin account:"
successfully_deleted_user: Succesvol gebruiker %{username} verwijderd
failed_to_delete_user: Mislukt de gebruiker %{username} te verwijderen
- openid_url_verified: Je hebt %{url} met succes geverifieerd als je identiteit en uw authenticatie type OpenID opgeslagen.
- destroy_successful: Gebruiker %{login} met succes verwijderd
total_projects: Totaal aantal projecten
signup_successful: Aanmelding succesvol voor gebruiker %{username}.
new_token_generated: Nieuwe token met succes gegenereerd
@@ -658,20 +692,20 @@ nl:
user_created: Gebruiker aangemaakt.
change_password_submit: Wachtwoord wijzigen
account_signup: Aanmelden voor een account
- manage_users: Beheren gebruikers
password_updated: Wachtwoord bijgewerkt.
+ manage_users: Beheren gebruikers
signup: Aanmelden
+ confirm_password: Bevestig wachtwoord
+ new_user_heading: "Registreer een nieuwe gebruiker:"
auth_type_updated: Authenticatietype bijgewerkt.
total_actions: Totaal aanal acties
desired_login: Gewenste login
- confirm_password: Bevestig wachtwoord
- new_user_heading: "Registreer een nieuwe gebruiker:"
- change_password_title: TRACKS::Wachtwoord wijzigen
- change_auth_type_title: TRACKS::Wijzig authenticatietype
- change_password_prompt: Voer uw nieuwe wachtwoord in de onderstaande velden in en kies 'Wachtwoord wijzigen' om uw huidige wachtwoord met uw nieuwe te vervangen.
password_confirmation_label: Bevestig wachtwoord
destroy_error: Er is een fout opgetreden bij het verwijderen van de gebruiker '%{login}'
choose_password: Kies een wachtwoord
+ change_password_title: TRACKS::Wachtwoord wijzigen
+ change_auth_type_title: TRACKS::Wijzig authenticatietype
+ change_password_prompt: Voer uw nieuwe wachtwoord in de onderstaande velden in en kies 'Wachtwoord wijzigen' om uw huidige wachtwoord met uw nieuwe te vervangen.
label_auth_type: Authenticatietype
new_password_label: Nieuw wachtwoord
register_with_cas: Met uw CAS gebruikersnaam
@@ -680,43 +714,12 @@ nl:
new_user_title: "TRACKS:: Aanmelden als de admin gebruiker"
destroy_confirmation: "Waarschuwing: dit zal de gebruiker '%{login} verwijderen met al zijn acties, contexten, projecten en notities. Weet u zeker dat u wilt doorgaan?"
signup_new_user: Registreer nieuwe gebruiker
- identity_url: Identiteit URL
- openid_ok_pref_failed: Je hebt succesvol de %{url} geverifieerd als je identiteit, maar er was een probleem met het opslaan van uw authenticatie voorkeuren.
- auth_change_submit: Wijzigen authenticatietype
change_authentication_type: Wijzigen authenticatietype
- select_authentication_type: Selecteer uw nieuwe authenticatie type en klik op 'Wijzigen authenticatietype' om uw huidige instellingen te vervangen.
+ identity_url: Identiteit URL
+ auth_change_submit: Wijzigen authenticatietype
+ openid_ok_pref_failed: Je hebt succesvol de %{url} geverifieerd als je identiteit, maar er was een probleem met het opslaan van uw authenticatie voorkeuren.
total_notes: Totaal aantal notities
- feedlist:
- actions_due_today: Acties die vandaag of eerder af moeten
- choose_context: Kies de context waar je een feed van wilt
- rss_feed: RSS Feed
- ical_feed: iCal feed
- legend: Legenda
- all_contexts: Alle contexten
- all_projects: Alle projecten
- choose_project: Kies het project waar je een feed van wilt
- active_projects_wo_next: Actieve projecten zonder acties
- project_needed: "Er moet ten minste \xC3\xA9\xC3\xA9n project zijn voor een feed opgevraagd kan worden"
- select_feed_for_project: Kies de feed voor dit project
- active_starred_actions: Alle gesterde, actieve acties
- context_needed: "Er moet eerst ten minste \xC3\xA9\xC3\xA9n context zijn voor je een feed kan opvragen"
- select_feed_for_context: Kies de feed voor deze context
- projects_and_actions: Actieve projecten met hun acties
- actions_due_next_week: Acties die binnen 7 dagen afgerond moeten
- notice_incomplete_only: "Merk op: alle feeds laten alleen acties zien die niet afgerond zijn, tenzij anders vermeld."
- last_fixed_number: Laatste %{number} acties
- all_actions: Alle acties
- actions_completed_last_week: Acties afgerond in de afgelopen 7 dagen
- context_centric_actions: Feeds voor onafgeronde acties in een specifieke context
- plain_text_feed: Reguliere tekst feed
- project_centric: Feeds voor onafgeronde acties in een specifiek project
- sidebar:
- list_name_active_contexts: Actieve contexten
- list_name_active_projects: Actieve projecten
- list_empty: Geen
- list_name_completed_projects: Voltooide projecten
- list_name_hidden_projects: Verborgen projecten
- list_name_hidden_contexts: Verborgen contexten
+ select_authentication_type: Selecteer uw nieuwe authenticatie type en klik op 'Wijzigen authenticatietype' om uw huidige instellingen te vervangen.
contexts:
delete_context_title: Verwijder context
hide_form: Verberg formulier
@@ -728,20 +731,44 @@ nl:
hidden_contexts: Verborgen contexten
no_contexts_active: Momenteel zijn er geen actieve contexten
context_hide: Verberg van de start pagina?
+ visible_contexts: Zichtbare contexten
save_status_message: Context bewaard
show_form: Maak een nieuwe context
add_context: Context toevoegen
- visible_contexts: Zichtbare contexten
- update_status_message: Naam van de context was veranderd
context_name: Context naam
- new_context_post: "' zal ook gemaakt worden. Weet u dit zeker?"
+ update_status_message: Naam van de context was veranderd
status_active: Context is actief
+ new_context_post: "' zal ook gemaakt worden. Weet u dit zeker?"
+ context_deleted: De context '%{name}' is verwijderd
+ no_contexts_hidden: Momenteel zijn er geen verborgen contexten
new_context_pre: Nieuwe context '
no_actions: Momenteel zijn er geen onafgeronde acties in deze context
last_completed_in_context: in deze context (laatste %{number})
- context_deleted: De context '%{name}' is verwijderd
- no_contexts_hidden: Momenteel zijn er geen verborgen contexten
status_hidden: Context is verborgen
+ feedlist:
+ actions_due_today: Acties die vandaag of eerder af moeten
+ choose_context: Kies de context waar je een feed van wilt
+ ical_feed: iCal feed
+ all_contexts: Alle contexten
+ rss_feed: RSS Feed
+ legend: Legenda
+ all_projects: Alle projecten
+ choose_project: Kies het project waar je een feed van wilt
+ project_needed: "Er moet ten minste \xC3\xA9\xC3\xA9n project zijn voor een feed opgevraagd kan worden"
+ select_feed_for_project: Kies de feed voor dit project
+ active_projects_wo_next: Actieve projecten zonder acties
+ active_starred_actions: Alle gesterde, actieve acties
+ context_needed: "Er moet eerst ten minste \xC3\xA9\xC3\xA9n context zijn voor je een feed kan opvragen"
+ select_feed_for_context: Kies de feed voor deze context
+ projects_and_actions: Actieve projecten met hun acties
+ actions_due_next_week: Acties die binnen 7 dagen afgerond moeten
+ notice_incomplete_only: "Merk op: alle feeds laten alleen acties zien die niet afgerond zijn, tenzij anders vermeld."
+ context_centric_actions: Feeds voor onafgeronde acties in een specifieke context
+ plain_text_feed: Reguliere tekst feed
+ last_fixed_number: Laatste %{number} acties
+ all_actions: Alle acties
+ actions_completed_last_week: Acties afgerond in de afgelopen 7 dagen
+ project_centric: Feeds voor onafgeronde acties in een specifiek project
datetime:
prompts:
minute: Minuut
@@ -788,34 +815,34 @@ nl:
other: over %{count} jaren
half_a_minute: halve minuut
login:
- sign_in: Meld aan
- openid_identity_url_not_found: Sorry, geen gebruiker met die identiteit URL bestaat (% {identity_url})
- login_cas: Ga naar het CAS
user_no_expiry: Blijf ingelogd
- cas_logged_in_greeting: Hallo, % {username}! U bent geauthenticeerd.
- cas_no_user_found: Hallo,% {username}! Je hebt nog geen account op Tracks.
- cas_login: CAS Inloggen
+ login_cas: Ga naar het CAS
+ sign_in: Meld aan
+ openid_identity_url_not_found: Sorry, geen gebruiker met die identiteit URL bestaat (%{identity_url})
successful_with_session_info: "Login succesvol:"
please_login: Log in om Tracks te gebruiken
- cas_username_not_found: Sorry, geen gebruiker met die CAS gebruikersnaam bestaat (% {username})
- cas_create_account: Als u willen vragen ga hier om %{signup_link}
+ cas_logged_in_greeting: Hallo, %{username}! U bent geauthenticeerd.
+ cas_no_user_found: Hallo,%{username}! Je hebt nog geen account op Tracks.
+ cas_login: CAS Inloggen
+ cas_username_not_found: Sorry, geen gebruiker met die CAS gebruikersnaam bestaat (%{username})
mobile_use_openid: ... if inloggen met een OpenID
+ cas_create_account: Als u willen vragen ga hier om %{signup_link}
account_login: Account login
cas_signup_link: Aanvragen account
- successful: Succesvol aangemeld. Welkom terug!
session_will_not_expire: sessie zal niet verlopen.
- session_time_out: Sessie is verlopen. Gelieve % {link}
+ successful: Succesvol aangemeld. Welkom terug!
session_will_expire: sessie zal verlopen na %{hours} u(u)r(en) van inactiviteit.
option_separator: of,
+ session_time_out: Sessie is verlopen. Gelieve %{link}
login_standard: Ga terug naar de standaard login
+ log_in_again: opnieuw in te loggen.
+ logged_out: Je bent afgemeld bij Tracks.
login_with_openid: inloggen met een OpenID
unsuccessful: Login mislukt.
- log_in_again: log opnieuw in.
- logged_out: Je bent afgemeld bij Tracks.
search:
contexts_matching_query: Contexten passend bij zoekopdracht
tags_matching_query: Tags passend bij zoekopdracht
- no_results: Uw zoekopdracht heeft geen resultaten opgeleverd.
todos_matching_query: Todos passend bij zoekopdracht
projects_matching_query: Projecten passend bij zoekopdracht
notes_matching_query: Notities passend bij zoekopdracht
+ no_results: Uw zoekopdracht heeft geen resultaten opgeleverd.
diff --git a/db/tracks-20-blank.sqlite3 b/db/tracks-20-blank.sqlite3.db
similarity index 61%
rename from db/tracks-20-blank.sqlite3
rename to db/tracks-20-blank.sqlite3.db
index b5bd761f..d3aa896f 100644
Binary files a/db/tracks-20-blank.sqlite3 and b/db/tracks-20-blank.sqlite3.db differ
diff --git a/db/tracks-17-example.db b/db/tracks-20-example.sqlite3.db
similarity index 84%
rename from db/tracks-17-example.db
rename to db/tracks-20-example.sqlite3.db
index d892ce77..665c2c3b 100644
Binary files a/db/tracks-17-example.db and b/db/tracks-20-example.sqlite3.db differ
diff --git a/db/tracks-17-test.db b/db/tracks-20-test.sqlite3.db
similarity index 63%
rename from db/tracks-17-test.db
rename to db/tracks-20-test.sqlite3.db
index 3f79ee53..f1704331 100644
Binary files a/db/tracks-17-test.db and b/db/tracks-20-test.sqlite3.db differ
diff --git a/db/tracks_1.7_content.sql b/db/tracks_1.7_content.sql
deleted file mode 100644
index 150448ba..00000000
--- a/db/tracks_1.7_content.sql
+++ /dev/null
@@ -1,57 +0,0 @@
--- Dump of table contents
--- Sample data to populate your database
--- No data is included for users: create your own users via the http://YOURURL/signup page
-
--- Dump of table contexts
--- ------------------------------------------------------------
-
-INSERT INTO "contexts" VALUES(1,'agenda',1,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(2,'call',2,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(3,'email',3,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(4,'errand',4,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(5,'lab',5,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(6,'library',6,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(7,'freetime',7,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(8,'office',8,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-INSERT INTO "contexts" VALUES(9,'waiting for',9,'f',1,'2008-02-25 20:21:09','2008-03-24 19:23:53');
-
--- Dump of table notes
--- ------------------------------------------------------------
-
-INSERT INTO "notes" VALUES(1,1,1,'Need to collect a catalogue from Time Machines R Us','2006-06-10 14:36:02','2006-06-10 14:36:02');
-INSERT INTO "notes" VALUES(2,1,1,'Should I go for a swirly effect or a whooshy one?','2006-06-10 14:36:02','2006-06-10 14:36:02');
-
-
--- Dump of table projects
--- ------------------------------------------------------------
-
-INSERT INTO "projects" VALUES(1,'Build a working time machine',1,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
-INSERT INTO "projects" VALUES(2,'Make more money than Billy Gates',2,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
-INSERT INTO "projects" VALUES(3,'Evict dinosaurs from the garden',3,1,'','active','2008-02-25 20:21:09','2008-03-24 19:23:53',NULL,NULL);
-
-
--- Dump of table schema_info
--- ------------------------------------------------------------
-
-INSERT INTO "schema_migrations" VALUES('44');
-
-
--- Dump of table todos
--- ------------------------------------------------------------
-
-INSERT INTO "todos" VALUES(1,1,2,'Call Bill Gates to find out how much he makes per day',NULL,'2006-06-03 14:36:02','2006-06-23 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-03 14:36:02');
-INSERT INTO "todos" VALUES(2,2,3,'Call dinosaur exterminator','Ask him if I need to hire a skip for the corpses.','2006-06-10 14:36:02','2006-06-23 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(3,4,NULL,'Buy milk',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(4,4,NULL,'Buy bread',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(5,5,1,'Construct time dilation device',NULL,'2006-06-10 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(6,2,1,'Phone Grandfather to ask about the paradox','Added some _notes_.','2006-06-10 14:36:02','2006-06-02 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(7,6,3,'Get a book out of the library','Dinosaurs''R','2006-06-10 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(8,4,NULL,'Upgrade to Rails 0.9.1',NULL,'2006-06-10 14:36:02','2006-06-09 23:00:00',NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(9,1,NULL,'This should be due today',NULL,'2006-06-10 14:36:02','2006-06-09 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(10,1,NULL,'foo',NULL,'2006-06-10 14:36:02','2005-01-05 00:00:00',NULL,1,NULL,'completed',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(11,1,2,'Buy shares',NULL,'2006-06-10 14:36:02','2005-02-01 00:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(12,1,3,'Buy stegosaurus bait',NULL,'2006-06-10 14:36:02','2006-06-16 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(13,1,3,'New action in context','Some notes','2006-06-10 14:36:02','2006-06-16 23:00:00',NULL,1,NULL,'active',NULL,'2006-06-10 14:36:02');
-INSERT INTO "todos" VALUES(14,2,2,'Call stock broker','tel: 12345','2006-06-03 14:36:02',NULL,NULL,1,NULL,'active',NULL,'2006-06-03 14:36:02');
-
-
diff --git a/doc/CHANGELOG b/doc/CHANGELOG
index 0d8351f9..e18e714c 100644
--- a/doc/CHANGELOG
+++ b/doc/CHANGELOG
@@ -9,16 +9,22 @@
* Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss
* Original developer: bsag (http://www.rousette.org.uk/)
* Contributors: http://getontracks.org/wiki/Contributors
-* Version: 2.0RC1
-* Copyright: (cc) 2004-2010 rousette.org.uk.
+* Version: 2.0devel
+* Copyright: (cc) 2004-2011 rousette.org.uk.
* License: GNU GPL
== Version 2.0 (RC)
+(RC1 was never release, but was mentioned in the DOCS for while. People are
+ referencing RC1 with bug report. So for the release we've bumped the version
+ to RC2)
+
New features:
1. Redesign of menus and introduction of a context menu per todo
-2. You can now set the default tags for a project which are added automatically to a new todo in that project if no tags are supplied
-3. Tracks now includes support of dependencies. Making an action dependent on another action will hide it until the dependency is completed
+2. You can now set the default tags for a project which are added automatically
+ to a new todo in that project if no tags are supplied
+3. Tracks now includes support of dependencies. Making an action dependent on
+ another action will hide it until the dependency is completed
4. you can now drag an action from one context to another
5. Support for entering multiple actions in one form
6. You can now promote an action to a project
@@ -26,16 +32,21 @@ New features:
8. The project description supports markup
9. support for Mail.app (message://) and OneNote (onenote://) links in notes
10.The email receiver is now able to receive email from several email adresses.
- In site.yml this could be set to the previous behavior (receive from one address per user)
+ In site.yml this could be set to the previous behavior (receive from one
+ address per user)
11.You can enable open signup (like in tracks.tra.in)
12.Cleanup of context page
13.Support for CAS for login
-14.Support for adding Tracks as a GMail Widget with instructions on the Integrations page
+14.Support for adding Tracks as a GMail Widget with instructions on the
+ Integrations page
+15.Tracks now support internationalization. First translations are German and
+ Dutch. See http://www.getontracks.org/wiki/Translating-Tracks it you like to
+ help translating Tracks to other languages
Under the hood
1. All js is migrated to jQuery and most ui-widgets are migrated to jQuery-UI
2. Cucumber is added for integration testing. The RSpec stories are migrated to cucumber
-3. Upgraded to rails 2.3.5 (newer versions unfortunately conflict with cucumber)
+3. Upgraded to rails 2.3.11 and upgraded most gems/plugins
3. Bugfixes (lots of them)
== Version 1.7
diff --git a/doc/README_DEVELOPERS b/doc/README_DEVELOPERS
index ecdf371f..99606779 100644
--- a/doc/README_DEVELOPERS
+++ b/doc/README_DEVELOPERS
@@ -36,7 +36,7 @@ If you want to run tests using another database, that's fine, too. Just change y
This testing style is deprecated and are being moved over to Selenium via Cucumber.
-See the wiki for more information to run the tests that are not yet migrated
+See the wiki for more information to run the tests that are not yet migrated: http://www.getontracks.org/wiki/Testing
6. RSPEC tests
diff --git a/doc/tracks_api_wrapper.rb b/doc/tracks_api_wrapper.rb
index d52071a5..db01b041 100644
--- a/doc/tracks_api_wrapper.rb
+++ b/doc/tracks_api_wrapper.rb
@@ -1,5 +1,7 @@
require 'activeresource'
+# WARNING: this is not really maintained or tested...
+
# Install the ActiveResource gem if you don't already have it:
#
# sudo gem install activeresource --source http://gems.rubyonrails.org --include-dependencies
diff --git a/features/calendar.feature b/features/calendar.feature
index 3e6891f8..5343bc9c 100644
--- a/features/calendar.feature
+++ b/features/calendar.feature
@@ -1,4 +1,4 @@
-Feature: dependencies
+Feature: Show all due actions in a calendar view
As a Tracks user
In order to keep overview of my due todos
I want to manage due todos in a calendar view
@@ -8,12 +8,50 @@ Feature: dependencies
| login | password | is_admin |
| testuser | secret | false |
And I have logged in as "testuser" with password "secret"
+ And I have a context called "@calendar"
+ @selenium
Scenario: Setting due date of a todo will show it in the calendar
- Given this is a pending scenario
+ When I submit a new action with description "a new next action" in the context "@calendar"
+ And I go to the calendar page
+ Then the badge should show 0
+ And I should not see "a new next action"
+ When I go to the home page
+ And I edit the due date of "a new next action" to tomorrow
+ And I go to the calendar page
+ Then the badge should show 1
+ And I should see "a new next action"
+ @selenium
Scenario: Clearing the due date of a todo will remove it from the calendar
- Given this is a pending scenario
+ When I go to the home page
+ And I submit a new action with description "a new next action" in the context "@calendar"
+ And I edit the due date of "a new next action" to tomorrow
+ And I go to the calendar page
+ Then I should see "a new next action"
+ When I clear the due date of "a new next action"
+ Then I should not see "a new next action"
+ @selenium
+ Scenario: Marking a todo complete will remove it from the calendar
+ Given I have a todo "a new next action" in the context "@calendar" which is due tomorrow
+ When I go to the calendar page
+ Then I should see "a new next action"
+ When I clear the due date of "a new next action"
+ Then I should not see "a new next action"
+
+ @selenium
+ Scenario: Deleting a todo complete will remove it from the calendar
+ Given I have a todo "a new next action" in the context "@calendar" which is due tomorrow
+ When I go to the calendar page
+ Then I should see "a new next action"
+ When I delete the action "a new next action"
+ Then I should not see "a new next action"
+
+ @selenium
Scenario: Changing due date of a todo will move it in the calendar
- Given this is a pending scenario
+ Given I have a todo "a new next action" in the context "@calendar" which is due tomorrow
+ When I go to the calendar page
+ Then I should see "a new next action"
+ When I edit the due date of "a new next action" to next month
+ Then I should see "a new next action" in the due next month container
diff --git a/features/dependencies.feature b/features/dependencies.feature
index 1cef6d98..b15f6377 100644
--- a/features/dependencies.feature
+++ b/features/dependencies.feature
@@ -23,22 +23,6 @@ Feature: dependencies
When I expand the dependencies of "Todo 2"
Then I should see "Todo 3" within the dependencies of "Todo 2"
- @selenium
- Scenario: Adding dependency with comma to todo # for #975
- Given I have a context called "@pc"
- And I have a project "dependencies" that has the following todos
- | description | context |
- | test,1, 2,3 | @pc |
- | test me | @pc |
- When I visit the "dependencies" project
- And I drag "test me" to "test,1, 2,3"
- Then the successors of "test,1, 2,3" should include "test me"
- When I edit the dependency of "test me" to "'test,1, 2,3' <'@pc'; 'dependencies'>,'test,1, 2,3' <'@pc'; 'dependencies'>"
- Then there should not be an error
-
- Scenario: Deleting a predecessor will activate successors
- Given this is a pending scenario
-
@selenium
Scenario: I can edit a todo to add the todo as a dependency to another
Given I have a context called "@pc"
@@ -48,14 +32,14 @@ Feature: dependencies
| test 2 | @pc |
| test 3 | @pc |
When I visit the "dependencies" project
- When I edit the dependency of "test 1" to "'test 2' <'@pc'; 'dependencies'>"
+ When I edit the dependency of "test 1" to add "test 2" as predecessor
Then I should see "test 1" within the dependencies of "test 2"
And I should see "test 1" in the deferred container
- When I edit the dependency of "test 1" to "'test 2' <'@pc'; 'dependencies'>, 'test 3' <'@pc'; 'dependencies'>"
+ When I edit the dependency of "test 1" to add "test 3" as predecessor
Then I should see "test 1" within the dependencies of "test 2"
Then I should see "test 1" within the dependencies of "test 3"
- When I edit the dependency of "test 1" to "'test 2' <'@pc'; 'dependencies'>"
- And I edit the dependency of "test 2" to "'test 3' <'@pc'; 'dependencies'>"
+ When I edit the dependency of "test 1" to remove "test 3" as predecessor
+ And I edit the dependency of "test 2" to add "test 3" as predecessor
Then I should see "test 1" within the dependencies of "test 3"
Then I should see "test 2" within the dependencies of "test 3"
@@ -69,12 +53,44 @@ Feature: dependencies
And "test 1" depends on "test 2"
When I visit the "dependencies" project
Then I should see "test 1" in the deferred container
- When I edit the dependency of "test 1" to ""
+ When I edit the dependency of "test 1" to remove "test 2" as predecessor
Then I should not see "test 1" within the dependencies of "test 2"
And I should not see "test 1" in the deferred container
+ @selenium
+ Scenario: Completing a predecessor will activate successors
+ Given I have a context called "@pc"
+ And I have a project "dependencies" that has the following todos
+ | description | context |
+ | test 1 | @pc |
+ | test 2 | @pc |
+ | test 3 | @pc |
+ And "test 2" depends on "test 1"
+ When I visit the "dependencies" project
+ Then I should see "test 2" in the deferred container
+ And I should see "test 1" in the action container
+ When I mark "test 1" as complete
+ Then I should see "test 1" in the completed container
+ And I should see "test 2" in the action container
+ And I should not see "test 2" in the deferred container
+ And I should see the empty message in the deferred container
+
+ @selenium
Scenario: Deleting a predecessor will activate successors
- Given this is a pending scenario
+ Given I have a context called "@pc"
+ And I have a project "dependencies" that has the following todos
+ | description | context |
+ | test 1 | @pc |
+ | test 2 | @pc |
+ | test 3 | @pc |
+ And "test 2" depends on "test 1"
+ When I visit the "dependencies" project
+ Then I should see "test 2" in the deferred container
+ And I should see "test 1" in the action container
+ When I delete the action "test 1"
+ And I should see "test 2" in the action container
+ And I should not see "test 2" in the deferred container
+ And I should see the empty message in the deferred container
Scenario: Deleting a successor will update predecessor
- Given this is a pending scenario
\ No newline at end of file
+ Given this is a pending scenario
diff --git a/features/edit_a_todo.feature b/features/edit_a_todo.feature
index 96b657a4..2d70fddd 100644
--- a/features/edit_a_todo.feature
+++ b/features/edit_a_todo.feature
@@ -12,12 +12,12 @@ Feature: Edit a next action from every page
Scenario: I can toggle the star of a todo
Given this is a pending scenario
- @selenium @wip
+ @selenium
Scenario: I can delete a todo
- Given I have a todo with description "delete me" in the context "@home"
+ Given I have a todo "delete me" in the context "@home"
When I go to the home page
Then I should see "delete me"
- And I delete the todo
+ When I delete the action "delete me"
Then I should not see "delete me"
Scenario: Removing the last todo in context will hide context # delete, edit
@@ -26,24 +26,25 @@ Feature: Edit a next action from every page
Scenario: Deleting the last todo in container will show empty message # only project, context, tag, not todo
Given this is a pending scenario
- @selenium @wip
+ @selenium
Scenario Outline: I can mark an active todo complete and it will update empty messages
+ Given I have a context called "visible context"
+ And I have a project called "visible project"
When I go to the
Then I should see ""
- When I submit a new action with description "visible todo" to project "visible project" with tags "test" in the context "visible context"
+ When I submit a new action with description "visible todo" to project "visible project" with tags "starred" in the context "visible context"
Then I should see "visible todo"
And I should not see ""
- When I mark the todo complete
- Then I should not see "visible context"
+ When I mark "visible todo" as complete
And I should see ""
- And I should see "visible todo" in the completed todos container
+ And I should see "visible todo" in the completed container
Scenarios:
- | page | empty message |
- | tag page for "starred" | No actions found |
- | home page | No actions found |
- | context page for "visible context" | Currently there are no deferred or pending actions |
- | project page for "visible project" | Currently there are no deferred or pending actions |
+ | page | empty message |
+ | "visible project" project | Currently there are no incomplete actions in this project |
+ | home page | Currently there are no incomplete actions |
+ | tag page for "starred" | No actions found |
+ | context page for "visible context" | Currently there are no incomplete actions in this context |
@selenium @wip
Scenario Outline: I can mark a deferred todo complete and it will update empty messages
@@ -52,7 +53,7 @@ Feature: Edit a next action from every page
When I submit a new deferred action with description "visible todo" to project "visible project" with tags "test" in the context "visible context"
Then I should see "visible todo"
And I should not see ""
- When I mark the todo complete
+ When I mark "visible todo" complete
Then I should not see "visible context"
And I should see ""
And I should see "visible todo" in the completed todos container
@@ -63,10 +64,6 @@ Feature: Edit a next action from every page
| context page for "visible context" | Currently there are no deferred or pending actions |
| project page for "visible project" | Currently there are no deferred or pending actions |
- @selenium @wip
- Scenario: I can mark a deferred todo complete and it will update empty messages
- Given this is a pending scenario
-
@selenium @wip
Scenario Outline: I can mark a completed todo active and it will update empty messages
Given I have a completed todo with description "visible todo" to project "visible project" with tags "test" in the context "visible context"
diff --git a/features/mobile_add_action.feature b/features/mobile_add_action.feature
new file mode 100644
index 00000000..80d9939d
--- /dev/null
+++ b/features/mobile_add_action.feature
@@ -0,0 +1,36 @@
+Feature: Add new next action from mobile page
+ In order to be able to add next actions from the mobile interface
+ As a Tracks user
+ I want to to be able to add a new next actions from the mobile interface and prepopulate the context and / or project of the prior page
+
+ Background:
+ Given the following user record
+ | login | password | is_admin |
+ | testuser | secret | false |
+ And I am working on the mobile interface
+ And I have logged in as "testuser" with password "secret"
+ And I have a context called "a context"
+ And I have a project "test project" with a default context of "test context"
+
+ Scenario Outline: The new action form is prefilled with context and project
+ Given I am on the
+ When I follow "0-New action"
+ Then the selected project should be ""
+ And the selected context should be ""
+
+ Scenarios: # empty means no selected, i.e. first in list is shown
+ | page | project | context |
+ | home page | | |
+ | tickler page | | |
+ | "test project" project | test project | |
+ | context page for "test context" | | test context |
+ | tag page for "starred" | | |
+
+ Scenario: I can add a new todo using the mobile interface
+ Given I am on the home page
+ Then the badge should show 0
+ When I follow "0-New action"
+ And I fill in "Description" with "test me"
+ And I press "Create"
+ Then I should see "test me"
+ And the badge should show 1
diff --git a/features/mobile_context_list.feature b/features/mobile_context_list.feature
new file mode 100644
index 00000000..769c10b7
--- /dev/null
+++ b/features/mobile_context_list.feature
@@ -0,0 +1,23 @@
+Feature: View the list of contexts from mobile
+ In order to be able to see all contexts from the mobile interface
+ As a Tracks user
+ I want to to be able to see a list of project
+
+ Background:
+ Given the following user record
+ | login | password | is_admin |
+ | testuser | secret | false |
+ And I am working on the mobile interface
+ And I have logged in as "testuser" with password "secret"
+ And I have a context called "@mobile"
+ And I have a project "test project" that has the following todos
+ | context | description |
+ | @mobile | test action |
+
+ Scenario: I can go to a context from the mobile context list page
+ Given I have a todo "test mobile page" in the context "@mobile"
+ And I am on the contexts page
+ Then I should see "@mobile"
+ When I follow "@mobile"
+ Then the badge should show 2
+ And I should see "@mobile"
diff --git a/features/mobile_edit_a_todo.feature b/features/mobile_edit_a_todo.feature
new file mode 100644
index 00000000..62b8ad77
--- /dev/null
+++ b/features/mobile_edit_a_todo.feature
@@ -0,0 +1,38 @@
+Feature: Edit a next action from the mobile view
+ In order to manage a next action
+ As a Tracks user
+ I want to to be able to edit a next action
+
+ Background:
+ Given the following user record
+ | login | password | is_admin |
+ | testuser | secret | false |
+ And I am working on the mobile interface
+ And I have logged in as "testuser" with password "secret"
+ And I have a context called "@mobile"
+ And I have a project "test project" that has the following todos
+ | context | description |
+ | @mobile | test action |
+
+ Scenario: I can edit an action on the mobile page
+ When I am on the home page
+ Then the badge should show 1
+ Then I should see "test action"
+ When I follow "test action"
+ And I fill in "Description" with "changed action"
+ And I press "Update"
+ Then I should see "changed action"
+ And I should not see "test action"
+ When I follow "changed action"
+ And I check "done"
+ And I press "Update"
+ Then I should not see "changed action"
+
+ Scenario: Navigate from home page
+ move this to separate features when other scenarios are created for these features
+ When I am on the home page
+ Then the badge should show 1
+ When I follow "Tickler"
+ Then the badge should show 0
+ When I follow "Feeds"
+ Then I should see "Last 15 actions"
diff --git a/features/mobile_project_list.feature b/features/mobile_project_list.feature
new file mode 100644
index 00000000..35402a49
--- /dev/null
+++ b/features/mobile_project_list.feature
@@ -0,0 +1,22 @@
+Feature: View the list of projects from mobile
+ In order to be able to see all project from the mobile interface
+ As a Tracks user
+ I want to to be able to see a list of project
+
+ Background:
+ Given the following user record
+ | login | password | is_admin |
+ | testuser | secret | false |
+ And I am working on the mobile interface
+ And I have logged in as "testuser" with password "secret"
+ And I have a context called "@mobile"
+ And I have a project "test project" that has the following todos
+ | context | description |
+ | @mobile | test action |
+
+ Scenario: I can go to a project from the list of project in mobile view
+ Given I am on the projects page
+ Then I should see "test project"
+ When I follow "test project"
+ Then the badge should show 1
+ And I should see "test action"
diff --git a/features/mobile_tagging_todos.feature b/features/mobile_tagging_todos.feature
new file mode 100644
index 00000000..c12ffbb8
--- /dev/null
+++ b/features/mobile_tagging_todos.feature
@@ -0,0 +1,24 @@
+Feature: Show the actions that are tagged on the mobile page
+ In order to be able to see all actions tags with a certain tag
+ As a Tracks user
+ I want to to be able to find all actions with a specific tag
+
+ Background:
+ Given the following user record
+ | login | password | is_admin |
+ | testuser | secret | false |
+ And I am working on the mobile interface
+ And I have logged in as "testuser" with password "secret"
+ And I have a context called "@mobile"
+ And I have a project "my project" that has the following todos
+ | context | description | tags |
+ | @mobile | first action | test, bla |
+ | @mobile | second action | bla |
+
+ Scenario: I can follow the tag of a action to see all actions belonging to that todo
+ When I go to the home page
+ And I follow "test"
+ Then the badge should show 1
+ When I go to the home page
+ And I follow "bla"
+ Then the badge should show 2
diff --git a/features/project_edit.feature b/features/project_edit.feature
index 86696224..4b924442 100644
--- a/features/project_edit.feature
+++ b/features/project_edit.feature
@@ -12,13 +12,13 @@ Feature: Edit a project
Scenario: I can go to the note of a project
Given I have a project "test" with 2 notes
- When I visit the "test" project
+ When I go to the "test" project
When I click on the first note icon
Then I should go to that note page
@selenium
Scenario: I can describe the project using markup
- When I visit the "manage me" project
+ When I go to the "manage me" project
And I edit the project description to "_successfull outcome_: project is *done*"
Then I should see the italic text "successfull outcome" in the project description
And I should see the bold text "done" in the project description
@@ -26,7 +26,7 @@ Feature: Edit a project
@selenium
Scenario: I can edit the project name in place
Given I have a project "release tracks 1.8" with 1 todos
- When I visit the project page for "release tracks 1.8"
+ When I go to the "release tracks 1.8" project
And I edit the project name in place to be "release tracks 2.0"
Then I should see the project name is "release tracks 2.0"
When I go to the projects page
@@ -37,7 +37,7 @@ Feature: Edit a project
@selenium
Scenario: I can change the name of the project using the Edit Project Settings form
Given I have a project "bananas" with 1 todos
- When I visit the "bananas" project
+ When I go to the "bananas" project
And I edit the project name to "cherries"
Then the project title should be "cherries"
@@ -63,21 +63,21 @@ Feature: Edit a project
@selenium
Scenario: I can add a note to the project
Given I have a project called "test"
- When I visit the "test" project
+ When I go to the "test" project
And I add a note "hello I'm testing" to the project
Then I should see one note in the project
@selenium
Scenario: Cancelling adding a note to the project will remove form
Given I have a project called "test"
- When I visit the "test" project
+ When I go to the "test" project
And I cancel adding a note to the project
Then the form for adding a note should not be visible
@selenium
Scenario: Long notes in a project are shown cut off
Given I have a project called "test"
- When I visit the "test" project
+ When I go to the "test" project
And I add a note "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890TOO LONG" to the project
Then I should not see "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890TOO LONG"
And I should see "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456"
@@ -98,10 +98,11 @@ Feature: Edit a project
Given this is a pending scenario # empty message is in separate container
# Ticket #1043
- @selenium @wip
+ @selenium
Scenario: I can move a todo out of the current project
Given I have a project "foo" with 2 todos
- When I visit the "foo" project
+ And I have a project called "bar"
+ When I go to the "foo" project
And I change the project_name field of "Todo 1" to "bar"
- Then I should not see the todo "Todo 1"
- And I should see the todo "Todo 2"
+ Then I should not see "Todo 1"
+ And I should see "Todo 2"
diff --git a/features/step_definitions/dependencies_steps.rb b/features/step_definitions/dependencies_steps.rb
new file mode 100644
index 00000000..26833079
--- /dev/null
+++ b/features/step_definitions/dependencies_steps.rb
@@ -0,0 +1,116 @@
+Given /^"([^"]*)" depends on "([^"]*)"$/ do |successor_name, predecessor_name|
+ successor = Todo.find_by_description(successor_name)
+ predecessor = Todo.find_by_description(predecessor_name)
+
+ successor.add_predecessor(predecessor)
+ successor.state = "pending"
+ successor.save!
+end
+
+When /^I drag "(.*)" to "(.*)"$/ do |dragged, target|
+ drag_id = Todo.find_by_description(dragged).id
+ drop_id = Todo.find_by_description(target).id
+ drag_name = "xpath=//div[@id='line_todo_#{drag_id}']//img[@class='grip']"
+ drop_name = "xpath=//div[@id='line_todo_#{drop_id}']//div[@class='description']"
+
+ selenium.drag_and_drop_to_object(drag_name, drop_name)
+
+ arrow = "xpath=//div[@id='line_todo_#{drop_id}']/div/a[@class='show_successors']/img"
+ selenium.wait_for_element(arrow, :timeout_in_seconds => 5)
+end
+
+When /^I expand the dependencies of "([^\"]*)"$/ do |todo_name|
+ todo = Todo.find_by_description(todo_name)
+ todo.should_not be_nil
+
+ expand_img_locator = "xpath=//div[@id='line_todo_#{todo.id}']/div/a[@class='show_successors']/img"
+ selenium.click(expand_img_locator)
+end
+
+When /^I edit the dependency of "([^"]*)" to add "([^"]*)" as predecessor$/ do |todo_description, predecessor_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+ predecessor = @current_user.todos.find_by_description(predecessor_description)
+ predecessor.should_not be_nil
+
+ open_edit_form_for(todo)
+
+ input = "xpath=//form[@id='form_todo_#{todo.id}']//input[@id='predecessor_input']"
+ selenium.focus(input)
+ selenium.type_keys input, predecessor_description
+
+ # wait for auto complete
+ autocomplete = "xpath=//a[@id='ui-active-menuitem']"
+ selenium.wait_for_element(autocomplete, :timeout_in_seconds => 5)
+
+ # click first line
+ first_elem = "xpath=//ul/li[1]/a[@id='ui-active-menuitem']"
+ selenium.click(first_elem)
+
+ new_dependency_line = "xpath=//li[@id='pred_#{predecessor.id}']"
+ selenium.wait_for_element(new_dependency_line, :timeout_in_seconds => 5)
+
+ submit_edit_todo_form(todo)
+end
+
+When /^I edit the dependency of "([^"]*)" to remove "([^"]*)" as predecessor$/ do |todo_description, predecessor_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+ predecessor = @current_user.todos.find_by_description(predecessor_description)
+ predecessor.should_not be_nil
+
+ open_edit_form_for(todo)
+
+ delete_dep_button = "xpath=//form[@id='form_todo_#{todo.id}']//img[@id='delete_dep_#{predecessor.id}']"
+ selenium.click(delete_dep_button)
+ wait_for :timeout=>5 do
+ !selenium.is_element_present(delete_dep_button)
+ end
+
+ submit_edit_todo_form(todo)
+ # note that animations will be running after the ajax is completed
+end
+
+When /^I edit the dependency of "([^"]*)" to "([^"]*)"$/ do |todo_name, deps|
+ todo = @dep_todo = @current_user.todos.find_by_description(todo_name)
+ todo.should_not be_nil
+
+ open_edit_form_for(todo)
+ fill_in "predecessor_list_todo_#{todo.id}", :with => deps
+ submit_edit_todo_form(todo)
+end
+
+Then /^the successors of "(.*)" should include "(.*)"$/ do |parent_name, child_name|
+ parent = @current_user.todos.find_by_description(parent_name)
+ parent.should_not be_nil
+
+ child = parent.pending_successors.find_by_description(child_name)
+ child.should_not be_nil
+end
+
+Then /^I should see "([^\"]*)" within the dependencies of "([^\"]*)"$/ do |successor_description, todo_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+ successor = @current_user.todos.find_by_description(successor_description)
+ successor.should_not be_nil
+
+ # argh, webrat on selenium does not support within, so this won't work
+ # xpath = "//div[@id='line_todo_#{todo.id}'"
+ # Then "I should see \"#{successor_description}\" within \"xpath=#{xpath}\""
+
+ # let selenium look for the presence of the successor
+ xpath = "xpath=//div[@id='line_todo_#{todo.id}']//div[@id='successor_line_todo_#{successor.id}']//span"
+ selenium.wait_for_element(xpath, :timeout_in_seconds => 5)
+end
+
+Then /^I should not see "([^"]*)" within the dependencies of "([^"]*)"$/ do |successor_description, todo_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+ successor = @current_user.todos.find_by_description(successor_description)
+ successor.should_not be_nil
+ # let selenium look for the presence of the successor
+ xpath = "xpath=//div[@id='line_todo_#{todo.id}']//div[@id='successor_line_todo_#{successor.id}']//span"
+ wait_for :timeout => 5 do
+ !selenium.is_element_present(xpath)
+ end
+end
diff --git a/features/step_definitions/feedlist_steps.rb b/features/step_definitions/feedlist_steps.rb
index 1715f36e..043eee20 100644
--- a/features/step_definitions/feedlist_steps.rb
+++ b/features/step_definitions/feedlist_steps.rb
@@ -31,7 +31,7 @@ Then /^I should see "([^"]*)" as the selected context$/ do |context_name|
end
Then /^I should see feeds for "([^"]*)" in list of "([^"]*)"$/ do |name, list_type|
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
xpath= "//div[@id='feeds-for-#{list_type}']//strong"
name.should == response.selenium.get_text("xpath=#{xpath}")
end
diff --git a/features/step_definitions/generic_steps.rb b/features/step_definitions/generic_steps.rb
index 65b1439e..c21113a5 100644
--- a/features/step_definitions/generic_steps.rb
+++ b/features/step_definitions/generic_steps.rb
@@ -2,6 +2,10 @@ Given /this is a pending scenario/ do
pending
end
+Given /^I am working on the mobile interface$/ do
+ @mobile_interface = true
+end
+
Then /the badge should show (.*)/ do |number|
badge = -1
xpath= "//span[@id='badge_count']"
@@ -17,3 +21,16 @@ Then /the badge should show (.*)/ do |number|
badge.should == number.to_i
end
+
+Then /^I should see the empty message in the deferred container$/ do
+ wait_for :timeout => 5 do
+ selenium.is_visible("xpath=//div[@id='tickler']//div[@id='tickler-empty-nd']")
+ end
+end
+
+Then /^I should not see the context "([^"]*)"$/ do |context_name|
+ context = @current_user.contexts.find_by_name(context_name)
+ wait_for :timeout => 5 do
+ !selenium.is_visible("xpath=//div[@id='c#{context.id}']")
+ end
+end
diff --git a/features/step_definitions/login_steps.rb b/features/step_definitions/login_steps.rb
index 83af76a4..3509f959 100644
--- a/features/step_definitions/login_steps.rb
+++ b/features/step_definitions/login_steps.rb
@@ -1,5 +1,5 @@
Given /^I have logged in as "(.*)" with password "(.*)"$/ do |username, password|
- visit login_path
+ When "I go to the login page"
fill_in "Login", :with => username
fill_in "Password", :with => password
uncheck "Stay logged in:"
@@ -7,7 +7,8 @@ Given /^I have logged in as "(.*)" with password "(.*)"$/ do |username, password
if response.respond_to? :selenium
selenium.wait_for_page_to_load(5000)
end
- response.should contain(/Logout \(#{username}\)/)
+ logout_regexp = @mobile_interface ? "Logout" : "Logout \(#{username}\)"
+ response.should contain(logout_regexp)
@current_user = User.find_by_login(username)
end
diff --git a/features/step_definitions/project_list_steps.rb b/features/step_definitions/project_list_steps.rb
index 2ab5c737..3142e147 100644
--- a/features/step_definitions/project_list_steps.rb
+++ b/features/step_definitions/project_list_steps.rb
@@ -39,13 +39,13 @@ end
When /^I sort the active list alphabetically$/ do
click_link "Alphabetically"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
selenium.get_confirmation.should == "Are you sure that you want to sort these projects alphabetically? This will replace the existing sort order."
end
When /^I sort the list by number of tasks$/ do
click_link "By number of tasks"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
selenium.get_confirmation.should == "Are you sure that you want to sort these projects by the number of tasks? This will replace the existing sort order."
end
diff --git a/features/step_definitions/project_steps.rb b/features/step_definitions/project_steps.rb
index b01ce894..a74ca756 100644
--- a/features/step_definitions/project_steps.rb
+++ b/features/step_definitions/project_steps.rb
@@ -24,6 +24,13 @@ Given /^I have a project called "([^"]*)"$/ do |project_name|
Given "there exists a project \"#{project_name}\" for user \"#{@current_user.login}\""
end
+Given /^I have a project "([^"]*)" with a default context of "([^"]*)"$/ do |project_name, context_name|
+ Given "there exists a project \"#{project_name}\" for user \"#{@current_user.login}\""
+ context = @current_user.contexts.create!(:name => context_name)
+ @project.default_context = context
+ @project.save!
+end
+
Given /^I have the following projects:$/ do |table|
table.hashes.each do |project|
Given 'I have a project called "'+project[:project_name]+'"'
@@ -189,4 +196,4 @@ end
Then /^I should see the project name is "([^"]*)"$/ do |project_name|
Then "the project title should be \"#{project_name}\""
-end
\ No newline at end of file
+end
diff --git a/features/step_definitions/recurring_todo_steps.rb b/features/step_definitions/recurring_todo_steps.rb
index 76697b26..810b1e22 100644
--- a/features/step_definitions/recurring_todo_steps.rb
+++ b/features/step_definitions/recurring_todo_steps.rb
@@ -35,7 +35,7 @@ When /^I edit the name of the pattern "([^\"]*)" to "([^\"]*)"$/ do |pattern_nam
pattern.should_not be_nil
click_link "link_edit_recurring_todo_#{pattern.id}"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
fill_in "edit_recurring_todo_description", :with => new_name
selenium.click "recurring_todo_edit_action_submit"
@@ -49,7 +49,7 @@ When /^I star the pattern "([^\"]*)"$/ do |pattern_name|
pattern = @current_user.recurring_todos.find_by_description(pattern_name)
pattern.should_not be_nil
click_link "star_icon_#{pattern.id}"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
end
When /^I delete the pattern "([^"]*)"$/ do |pattern_name|
@@ -67,7 +67,7 @@ When /^I mark the pattern "([^"]*)" as complete$/ do |pattern_name|
pattern.should_not be_nil
pattern.completed?.should be_false
selenium.click "check_#{pattern.id}"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
end
When /^I mark the pattern "([^"]*)" as active$/ do |pattern_name|
@@ -75,7 +75,7 @@ When /^I mark the pattern "([^"]*)" as active$/ do |pattern_name|
pattern.should_not be_nil
pattern.completed?.should be_true
selenium.click "check_#{pattern.id}"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ wait_for_ajax
end
Then /^the state list "([^"]*)" should be empty$/ do |state|
diff --git a/features/step_definitions/todo_steps.rb b/features/step_definitions/todo_steps.rb
index a794b2f8..776d6374 100644
--- a/features/step_definitions/todo_steps.rb
+++ b/features/step_definitions/todo_steps.rb
@@ -2,15 +2,25 @@ Given /^I have no todos$/ do
Todo.delete_all
end
-Given /^I have a todo "(.*)"$/ do |description|
- context = @current_user.contexts.create!(:name => "context A")
+Given /^I have a todo "([^"]*)" in the context "([^"]*)"$/ do |description, context_name|
+ context = @current_user.contexts.find_or_create(:name => context_name)
@current_user.todos.create!(:context_id => context.id, :description => description)
end
+Given /^I have a todo "([^"]*)" in the context "([^"]*)" which is due tomorrow$/ do |description, context_name|
+ context = @current_user.contexts.find_or_create(:name => context_name)
+ @todo = @current_user.todos.create!(:context_id => context.id, :description => description)
+ @todo.due = @todo.created_at + 1.day
+ @todo.save!
+end
+
+Given /^I have a todo "([^"]*)"$/ do |description|
+ Given "I have a todo \"#{description}\" in the context \"Context A\""
+end
+
Given /^I have ([0-9]+) todos$/ do |count|
- context = @current_user.contexts.create!(:name => "context A")
count.to_i.downto 1 do |i|
- @current_user.todos.create!(:context_id => context.id, :description => "todo #{i}")
+ Given "I have a todo \"todo #{i}\" in the context \"Context A\""
end
end
@@ -42,57 +52,29 @@ Given /^I have ([0-9]+) completed todos with a note$/ do |count|
end
end
-Given /^"(.*)" depends on "(.*)"$/ do |successor_name, predecessor_name|
- successor = Todo.find_by_description(successor_name)
- predecessor = Todo.find_by_description(predecessor_name)
-
- successor.add_predecessor(predecessor)
- successor.state = "pending"
- successor.save!
-end
-
Given /^I have a project "([^"]*)" that has the following todos$/ do |project_name, todos|
Given "I have a project called \"#{project_name}\""
@project.should_not be_nil
todos.hashes.each do |todo|
- context_id = @current_user.contexts.find_by_name(todo[:context])
- context_id.should_not be_nil
- @current_user.todos.create!(
+ context = @current_user.contexts.find_by_name(todo[:context])
+ context.should_not be_nil
+ new_todo = @current_user.todos.create!(
:description => todo[:description],
- :context_id => context_id,
+ :context_id => context.id,
:project_id=>@project.id)
+ unless todo[:tags].nil?
+ new_todo.tag_with(todo[:tags])
+ end
end
end
-When /^I drag "(.*)" to "(.*)"$/ do |dragged, target|
- drag_id = Todo.find_by_description(dragged).id
- drop_id = Todo.find_by_description(target).id
- drag_name = "xpath=//div[@id='line_todo_#{drag_id}']//img[@class='grip']"
- drop_name = "xpath=//div[@id='line_todo_#{drop_id}']//div[@class='description']"
-
- selenium.drag_and_drop_to_object(drag_name, drop_name)
-
- arrow = "xpath=//div[@id='line_todo_#{drop_id}']/div/a[@class='show_successors']/img"
- selenium.wait_for_element(arrow, :timeout_in_seconds => 5)
-end
-
-When /^I expand the dependencies of "([^\"]*)"$/ do |todo_name|
- todo = Todo.find_by_description(todo_name)
- todo.should_not be_nil
-
- expand_img_locator = "xpath=//div[@id='line_todo_#{todo.id}']/div/a[@class='show_successors']/img"
- selenium.click(expand_img_locator)
-end
-
-When /I change the (.*) field of "([^\"]*)" to "([^\"]*)"$/ do |field, todo_name, new_value|
+When /I change the (.*) field of "([^\"]*)" to "([^\"]*)"$/ do |field_name, todo_name, new_value|
todo = @current_user.todos.find_by_description(todo_name)
todo.should_not be_nil
- selenium.click("//img[@id='edit_icon_todo_#{todo.id}']", :wait_for => :ajax, :javascript_framework => :jquery)
- selenium.type("css=form.edit_todo_form input[name=#{field}]", new_value)
- selenium.click("css=button.positive", :wait_for => :ajax, :javascript_framework => :jquery)
- # TODO: change to a wait_for
- sleep(5)
+ open_edit_form_for(todo)
+ selenium.type("css=form.edit_todo_form input[name=#{field_name}]", new_value)
+ submit_edit_todo_form(todo)
end
When /^I submit a new action with description "([^"]*)"$/ do |description|
@@ -165,15 +147,52 @@ When /^I submit the new multiple actions form with$/ do |multi_line_descriptions
submit_multiple_next_action_form
end
-When /^I edit the dependency of "([^"]*)" to "([^"]*)"$/ do |todo_name, deps|
- todo = @dep_todo = @current_user.todos.find_by_description(todo_name)
+When /^I edit the due date of "([^"]*)" to tomorrow$/ do |action_description|
+ todo = @current_user.todos.find_by_description(action_description)
todo.should_not be_nil
- # click edit
- selenium.click("//div[@id='line_todo_#{todo.id}']//img[@id='edit_icon_todo_#{todo.id}']", :wait_for => :ajax, :javascript_framework => :jquery)
- fill_in "predecessor_list_todo_#{todo.id}", :with => deps
-
+ open_edit_form_for(todo)
+ fill_in "due_todo_#{todo.id}", :with => format_date(todo.created_at + 1.day)
submit_edit_todo_form(todo)
- sleep(1) # TODO: replace with some wait_for
+end
+
+When /^I edit the due date of "([^"]*)" to next month$/ do |action_description|
+ todo = @current_user.todos.find_by_description(action_description)
+ todo.should_not be_nil
+ open_edit_form_for(todo)
+ fill_in "due_todo_#{todo.id}", :with => format_date(todo.created_at + 1.month)
+ submit_edit_todo_form(todo)
+end
+
+When /^I clear the due date of "([^"]*)"$/ do |action_description|
+ todo = @current_user.todos.find_by_description(action_description)
+ todo.should_not be_nil
+ open_edit_form_for(todo)
+ selenium.click("//div[@id='edit_todo_#{todo.id}']//a[@id='due_x_todo_#{todo.id}']/img", :wait_for => :ajax, :javascript_framework => :jquery)
+ submit_edit_todo_form(todo)
+end
+
+When /^I mark "([^"]*)" as complete$/ do |action_description|
+ # TODO: generalize. this currently only works for projects wrt xpath
+ todo = @current_user.todos.find_by_description(action_description)
+ todo.should_not be_nil
+ check "mark_complete_#{todo.id}"
+ wait_for :timeout => 5 do
+ !selenium.is_element_present("//div[@id='p#{todo.project.id}items']//div[@id='line_todo_#{todo.id}']")
+ end
+ # note that animations could be running after finishing this
+end
+
+When /^I delete the action "([^"]*)"$/ do |action_description|
+ todo = @current_user.todos.find_by_description(action_description)
+ todo.should_not be_nil
+
+ delete_todo_button = "xpath=//a[@id='delete_todo_#{todo.id}']/img"
+ selenium.click delete_todo_button
+ selenium.get_confirmation.should == "Are you sure that you want to delete the action '#{todo.description}'?"
+
+ wait_for :timeout => 5 do
+ !selenium.is_element_present("//div[@id='line_todo_#{todo.id}']")
+ end
end
Then /^I should see ([0-9]+) todos$/ do |count|
@@ -185,43 +204,11 @@ end
Then /^there should not be an error$/ do
sleep(5)
# form should be gone and thus no errors visible
- selenium.is_visible("edit_todo_#{@dep_todo.id}").should == false
+ wait_for :timeout => 5 do
+ !selenium.is_visible("edit_todo_#{@dep_todo.id}")
+ end
end
-Then /^the successors of "(.*)" should include "(.*)"$/ do |parent_name, child_name|
- parent = @current_user.todos.find_by_description(parent_name)
- parent.should_not be_nil
-
- child = parent.pending_successors.find_by_description(child_name)
- child.should_not be_nil
-end
-
-Then /^I should see "([^\"]*)" within the dependencies of "([^\"]*)"$/ do |successor_description, todo_description|
- todo = @current_user.todos.find_by_description(todo_description)
- todo.should_not be_nil
- successor = @current_user.todos.find_by_description(successor_description)
- successor.should_not be_nil
-
- # argh, webrat on selenium does not support within, so this won't work
- # xpath = "//div[@id='line_todo_#{todo.id}'"
- # Then "I should see \"#{successor_description}\" within \"xpath=#{xpath}\""
-
- # let selenium look for the presence of the successor
- xpath = "xpath=//div[@id='line_todo_#{todo.id}']//div[@id='successor_line_todo_#{successor.id}']//span"
- selenium.wait_for_element(xpath, :timeout_in_seconds => 5)
-end
-
-Then /^I should not see "([^"]*)" within the dependencies of "([^"]*)"$/ do |successor_description, todo_description|
- todo = @current_user.todos.find_by_description(todo_description)
- todo.should_not be_nil
- successor = @current_user.todos.find_by_description(successor_description)
- successor.should_not be_nil
- # let selenium look for the presence of the successor
- xpath = "xpath=//div[@id='line_todo_#{todo.id}']//div[@id='successor_line_todo_#{successor.id}']//span"
- selenium.is_element_present(xpath).should be_false
-end
-
-
Then /^I should see the todo "([^\"]*)"$/ do |todo_description|
selenium.is_element_present("//span[.=\"#{todo_description}\"]").should be_true
end
@@ -245,8 +232,10 @@ end
Then /^the container for the context "([^"]*)" should not be visible$/ do |context_name|
context = @current_user.contexts.find_by_name(context_name)
context.should_not be_nil
+
+ wait_for_ajax
+
xpath = "xpath=//div[@id=\"c#{context.id}\"]"
- selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
selenium.is_element_present(xpath).should be_false
end
@@ -260,7 +249,31 @@ Then /^I should see "([^"]*)" in the deferred container$/ do |todo_description|
xpath = "//div[@id='tickler']//div[@id='line_todo_#{todo.id}']"
- selenium.is_element_present(xpath).should be_true
+ wait_for :timeout => 5 do
+ selenium.is_element_present(xpath)
+ end
+end
+
+Then /^I should see "([^"]*)" in the action container$/ do |todo_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+
+ xpath = "//div[@id='p#{todo.project.id}items']//div[@id='line_todo_#{todo.id}']"
+
+ wait_for :timeout => 5 do
+ selenium.is_element_present(xpath)
+ end
+end
+
+Then /^I should see "([^"]*)" in the completed container$/ do |todo_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+
+ xpath = "//div[@id='completed_container']//div[@id='line_todo_#{todo.id}']"
+
+ wait_for :timeout => 5 do
+ selenium.is_element_present(xpath)
+ end
end
Then /^I should not see "([^"]*)" in the deferred container$/ do |todo_description|
@@ -269,5 +282,28 @@ Then /^I should not see "([^"]*)" in the deferred container$/ do |todo_descripti
xpath = "//div[@id='tickler']//div[@id='line_todo_#{todo.id}']"
- selenium.is_element_present(xpath).should be_false
+ wait_for :timeout => 5 do
+ !selenium.is_element_present(xpath)
+ end
+end
+
+Then /^I should see "([^"]*)" in the due next month container$/ do |todo_description|
+ todo = @current_user.todos.find_by_description(todo_description)
+ todo.should_not be_nil
+
+ xpath = "xpath=//div[@id='due_after_this_month']//div[@id='line_todo_#{todo.id}']"
+
+ wait_for :timeout => 5 do
+ !selenium.is_element_present(xpath)
+ end
+end
+
+Then /^the selected project should be "([^"]*)"$/ do |content|
+ # Works for mobile. TODO: make it work for both mobile and non-mobile
+ field_labeled("Project").element.search(".//option[@selected = 'selected']").inner_html.should =~ /#{content}/
+end
+
+Then /^the selected context should be "([^"]*)"$/ do |content|
+ # Works for mobile. TODO: make it work for both mobile and non-mobile
+ field_labeled("Context").element.search(".//option[@selected = 'selected']").inner_html.should =~ /#{content}/
end
diff --git a/features/support/paths.rb b/features/support/paths.rb
index 9df097ce..3ec5f54e 100644
--- a/features/support/paths.rb
+++ b/features/support/paths.rb
@@ -6,53 +6,54 @@ module NavigationHelpers
# step definition in web_steps.rb
#
def path_to(page_name)
+ options = @mobile_interface ? {:format => :m} : {}
case page_name
when /the home\s?page/
- root_path
+ root_path(options)
when /the statistics page/
- stats_path
+ stats_path(options)
when /the signup page/
- signup_path
+ signup_path(options)
when /the login page/
- login_path
+ login_path(options)
when /the notes page/
- notes_path
+ notes_path(options)
when /the contexts page/
- contexts_path
+ contexts_path(options)
when /the projects page/
- projects_path
+ projects_path(options)
when /the manage users page/
- users_path
+ users_path(options)
when /the repeating todos page/
- recurring_todos_path
+ recurring_todos_path(options)
when /the integrations page/
- integrations_path
+ integrations_path(options)
when /the tickler page/
- tickler_path
+ tickler_path(options)
when /the export page/
- data_path
+ data_path(options)
when /the preference page/
- preferences_path
+ preferences_path(options)
when /the rest api docs page/
- rest_api_docs_path
+ rest_api_docs_path(options)
when /the search page/
- search_path
+ search_path(options)
when /the starred page/
- tag_path("starred")
+ tag_path("starred", options)
when /the feeds page/
- feeds_path
+ feeds_path(options)
when /the context page for "([^\"]*)" for user "([^\"]*)"/i
- context_path(User.find_by_login($2).contexts.find_by_name($1))
+ context_path(User.find_by_login($2).contexts.find_by_name($1), options)
when /the context page for "([^\"]*)"/i
- context_path(@current_user.contexts.find_by_name($1))
+ context_path(@current_user.contexts.find_by_name($1), options)
when /the "([^\"]*)" project for user "([^\"]*)"/i
- project_path(User.find_by_login($2).projects.find_by_name($1))
+ project_path(User.find_by_login($2).projects.find_by_name($1), options)
when /the "([^\"]*)" project/i
@project = @current_user.projects.find_by_name($1)
- project_path(@project)
+ project_path(@project, options)
when /the tag page for "([^"]*)"/i
- tag_path($1)
+ tag_path($1, options)
# Add more mappings here.
# Here is an example that pulls values out of the Regexp:
diff --git a/features/support/world.rb b/features/support/world.rb
index 1ecc5a26..899fb801 100644
--- a/features/support/world.rb
+++ b/features/support/world.rb
@@ -5,6 +5,7 @@ module TracksStepHelper
def submit_next_action_form
selenium.click("xpath=//form[@id='todo-form-new-action']//button[@id='todo_new_action_submit']", :wait_for => :ajax, :javascript_framework => :jquery)
+ sleep(1)
end
def submit_new_context_form
@@ -17,6 +18,9 @@ module TracksStepHelper
def submit_edit_todo_form (todo)
selenium.click("//div[@id='edit_todo_#{todo.id}']//button[@id='submit_todo_#{todo.id}']", :wait_for => :ajax, :javascript_framework => :jquery)
+ wait_for do
+ !selenium.is_element_present("//form[@id='form_todo_#{todo.id}']")
+ end
end
def format_date(date)
@@ -36,6 +40,20 @@ module TracksStepHelper
execute_javascript("$('#todo_project_name').val('');")
end
+ def open_edit_form_for(todo)
+ edit_button = "xpath=//div[@id='line_todo_#{todo.id}']//img[@id='edit_icon_todo_#{todo.id}']"
+
+ wait_for :timeout => 5 do
+ selenium.is_element_present(edit_button)
+ end
+
+ selenium.click(edit_button, :wait_for => :ajax, :javascript_framework => :jquery)
+ end
+
+ def wait_for_ajax
+ selenium.wait_for :wait_for => :ajax, :javascript_framework => :jquery
+ end
+
end
World(TracksStepHelper)
\ No newline at end of file
diff --git a/lib/tracks/source_view.rb b/lib/tracks/source_view.rb
index 9505282b..8cb17ca8 100644
--- a/lib/tracks/source_view.rb
+++ b/lib/tracks/source_view.rb
@@ -12,6 +12,10 @@ module Tracks
def nil?
yield if @source_view.nil? && block_given?
end
+
+ def context
+ yield if :context == @source_view && block_given?
+ end
def method_missing(check_source_view,*args)
yield if check_source_view == @source_view && block_given?
@@ -61,4 +65,4 @@ module Tracks
end
-ActionController::Base.send(:include, Tracks::SourceViewSwitching::Controller)
\ No newline at end of file
+ActionController::Base.send(:include, Tracks::SourceViewSwitching::Controller)
diff --git a/public/images/icon_delete.png b/public/images/icon_delete.png
new file mode 100644
index 00000000..4d66447d
Binary files /dev/null and b/public/images/icon_delete.png differ
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
index 0b32a7e6..d964d8f7 100644
--- a/public/javascripts/application.js
+++ b/public/javascripts/application.js
@@ -87,6 +87,56 @@ var TracksForm = {
submit_with_ajax_and_block_element('form#todo-form-multi-new-action', $(this));
return false;
});
+ },
+ enable_dependency_delete: function() {
+ $('a[class=icon_delete_dep]').live('click', function() {
+ var form = $(this).parents('form').get(0);
+ var predecessor_list = $(form).find('input[name=predecessor_list]');
+ var id_list = split( predecessor_list.val() );
+
+ // remove from ul
+ $(form).find("li#pred_"+this.id).slideUp(500).remove();
+
+ // remove from array
+ var new_list = new Array();
+ while (id_list.length > 0) {
+ var elem = id_list.pop();
+ if (elem != this.id && elem != '' && elem != ' ') {
+ new_list.push ( elem );
+ }
+ }
+
+ // update id list
+ predecessor_list.val( new_list.join(", ") );
+
+ if (new_list.length == 0) {
+ $(form).find("label#label_for_predecessor_input").hide();
+ $(form).find("ul#predecessor_ul").hide();
+ }
+
+ return false; // prevent submit/follow link
+ })
+ },
+ generate_dependency_list: function(todo_id) {
+ if (spec_of_todo.length > 0) {
+ // find edit form
+ var form_selector = "#form_todo_"+todo_id;
+ var form = $(form_selector);
+
+ var predecessor_list = form.find('input[name=predecessor_list]');
+ var id_list = split( predecessor_list.val() );
+
+ var label = form.find("label#label_for_predecessor_input").first();
+ label.show();
+
+ while (id_list.length > 0) {
+ var elem = id_list.pop();
+ var new_li = TodoItems.generate_predecessor(elem, spec_of_todo[elem]);
+ var ul = form.find('ul#predecessor_ul');
+ ul.html(ul.html() + new_li);
+ form.find('li#pred_'+elem).show();
+ }
+ }
}
}
@@ -130,6 +180,54 @@ var TracksPages = {
set_page_badge: function(count) {
$('#badge_count').html(count);
},
+ setup_autocomplete_for_tag_list: function(id) {
+ $(id+':not(.ac_input)')
+ .bind( "keydown", function( event ) { // don't navigate away from the field on tab when selecting an item
+ if ( event.keyCode === $.ui.keyCode.TAB &&
+ $( this ).data( "autocomplete" ).menu.active ) {
+ event.preventDefault();
+ }
+ })
+ .autocomplete({
+ minLength: 0,
+ source: function( request, response ) {
+ var last_term = extractLast( request.term );
+ if (last_term != "" && last_term != " ")
+ $.ajax( {
+ url: relative_to_root('tags.autocomplete'),
+ dataType: 'json',
+ data: {
+ term: last_term
+ },
+ success: function(data, textStatus, jqXHR) {
+ // remove spinner as removing the class is not always done by response
+ $(id).removeClass('ui-autocomplete-loading');
+ response(data, textStatus, jqXHR); // call jquery callback to handle data
+ }
+ })
+ else {
+ // remove spinner as typing will always add the spinner
+ $(id).removeClass('ui-autocomplete-loading');
+ }
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function( event, ui ) {
+ var terms = split( this.value );
+ // remove the current input
+ terms.pop();
+ // add the selected item
+ terms.push( ui.item.value );
+ // add placeholder to get the comma-and-space at the end
+ //terms.push( "" );
+ this.value = terms.join( ", " );
+ return false;
+ },
+ selectFirst: true
+ });
+ },
setup_behavior: function () {
/* main menu */
$('ul.sf-menu').superfish({
@@ -168,16 +266,6 @@ var TracksPages = {
/* fade flashes and alerts in automatically */
$(".alert").fadeOut(8000);
-
- /* for edit project form and edit todo form
-* TODO: refactor to separate calls from project and todo */
- $('.edit-form a.negative').live('click', function(){
- $(this).parents('.edit-form').fadeOut(200, function () {
- $(this).parents('.list').find('.project').fadeIn(500);
- $(this).parents('.container').find('.item-show').fadeIn(500);
- })
- });
-
}
}
@@ -265,7 +353,7 @@ var TodoItems = {
getContextsForAutocomplete: function (term, element_to_block) {
var allContexts = null;
var params = default_ajax_options_for_scripts('GET', relative_to_root('contexts.autocomplete'), element_to_block);
- params.data = "term="+term;
+ params.data += "&term="+term;
params.dataType = "json";
params.async = false;
params.success = function(result){
@@ -286,6 +374,68 @@ var TodoItems = {
}
return confirm(i18n['contexts.new_context_pre'] + givenContextName + i18n['contexts.new_context_post']);
},
+ generate_predecessor: function(todo_id, todo_spec) {
+ var img = "";
+ var anchor = "" + img + "";
+ var li = "
"+ anchor +" "+ todo_spec + "
";
+ return li;
+ },
+ highlight_todo: function(id) {
+ $(id).effect('highlight', {}, 2000, function(){ });
+ },
+ setup_autocomplete_for_predecessor: function() {
+ $('input[name=predecessor_input]:not(.ac_input)')
+ .bind( "keydown", function( event ) { // don't navigate away from the field on tab when selecting an item
+ if ( event.keyCode === $.ui.keyCode.TAB &&
+ $( this ).data( "autocomplete" ).menu.active ) {
+ event.preventDefault();
+ }
+ })
+ .autocomplete({
+ minLength: 0,
+ source: function( request, response ) {
+ var term = request.term;
+ if (term != "" && term != " ")
+ $.getJSON( relative_to_root('auto_complete_for_predecessor'), {
+ term: term
+ }, response );
+ },
+ focus: function() {
+ // prevent value inserted on focus
+ return false;
+ },
+ select: function( event, ui ) {
+ // retrieve values from input fields
+ var todo_spec = ui.item.label
+ var todo_id = ui.item.value
+ var form = $(this).parents('form').get(0);
+ var predecessor_list = $(form).find('input[name=predecessor_list]')
+ var id_list = split( predecessor_list.val() );
+
+ // add the dependency to id list
+ id_list.push( todo_id );
+ predecessor_list.val( id_list.join( ", " ) );
+
+ // show the html for the list of deps
+ $(form).find('ul#predecessor_ul').show();
+ $(form).find("label#label_for_predecessor_input").show();
+ if (todo_spec.length > 35 && form.id == "todo-form-new-action") {
+ // cut off string only in new-todo-form
+ todo_spec = todo_spec.substring(0,40)+"...";
+ }
+ // show the new dep in list
+ var html = $(form).find('ul#predecessor_ul').html();
+ var new_li = TodoItems.generate_predecessor(todo_id, todo_spec);
+ $(form).find('ul#predecessor_ul').html(html + new_li);
+ $(form).find('li#pred_'+todo_id).slideDown(500);
+
+ $(form).find('input[name=predecessor_input]').val('');
+ $(form).find('input[name=predecessor_input]').focus();
+ return false;
+ },
+ selectFirst: true
+ });
+ },
setup_behavior: function() {
/* show the notes of a todo */
$(".show_notes").live('click', function () {
@@ -312,7 +462,12 @@ var TodoItems = {
/* set behavior for edit icon */
$(".item-container a.edit_item").live('click', function (ev){
- get_with_ajax_and_block_element(this.href, $(this).parents(".item-container"));
+ var ajax_options = default_ajax_options_for_scripts('GET', this.href, $(this).parents('.item-container'));
+ var id = this.id.substr(15);
+ ajax_options.complete.push( function(){
+ TracksForm.generate_dependency_list(id);
+ });
+ $.ajax(ajax_options);
return false;
});
@@ -320,7 +475,7 @@ var TodoItems = {
$('.item-container a.icon_delete_item').live('click', function(evt){
var confirm_message = $(this).attr("x_confirm_message")
if(confirm(confirm_message)){
- delete_with_ajax_and_block_element(this.href, $(this).parents('.project'));
+ delete_with_ajax_and_block_element(this.href, $(this).parents('.item-container'));
}
return false;
});
@@ -331,6 +486,14 @@ var TodoItems = {
return false;
});
+ // for cancelling edit todo form
+ $('form.edit_todo_form a.negative').live('click', function(){
+ $(this).parents('.edit-form').fadeOut(200, function () {
+ $(this).parents('.list').find('.project').fadeIn(500);
+ $(this).parents('.container').find('.item-show').fadeIn(500);
+ })
+ });
+
// defer a todo
$(".item-container a.icon_defer_item").live('click', function(ev){
if ($(this).attr("x_defer_alert") == "true")
@@ -348,6 +511,26 @@ var TodoItems = {
$.ajax(ajax_options);
return false;
});
+
+ TracksForm.enable_dependency_delete();
+ }
+}
+
+var ContextItems = {
+ setup_autocomplete_for_contexts: function(id) {
+ $(id).autocomplete({
+ source: relative_to_root('contexts.autocomplete'),
+ selectFirst: true
+ });
+ }
+}
+
+var ProjectItems = {
+ setup_autocomplete_for_projects: function(id) {
+ $(id).autocomplete({
+ source: relative_to_root('projects.autocomplete'),
+ selectFirst: true
+ });
}
}
@@ -444,6 +627,14 @@ var ProjectListPage = {
return false;
});
+ /* cancel edit project form */
+ $('form.edit-project-form a.negative').live('click', function(){
+ $(this).parents('.edit-form').fadeOut(200, function () {
+ $(this).parents('.list').find('.project').fadeIn(500);
+ $(this).parents('.container').find('.item-show').fadeIn(500);
+ })
+ });
+
/* submit project form after entering new project */
$("form#project_form button.positive").live('click', function (ev) {
submit_with_ajax_and_block_element('form.#project_form', $(this));
@@ -735,6 +926,13 @@ function setup_auto_refresh(interval){
});
}
+function split( val ) {
+ return val.split( /,\s*/ );
+}
+function extractLast( term ) {
+ return split( term ).pop();
+}
+
$.fn.clearForm = function() {
return this.each(function() {
var type = this.type, tag = this.tagName.toLowerCase();
@@ -749,6 +947,13 @@ $.fn.clearForm = function() {
});
};
+$.fn.clearDeps = function() {
+ $('ul#predecessor_ul', this).hide();
+ $("label#label_for_predecessor_input").hide();
+ $('ul#predecessor_ul', this).html("");
+ $('input[name=predecessor_list]').val("");
+}
+
/**************************************/
/* Tracks AJAX functions */
/**************************************/
@@ -758,24 +963,27 @@ function generic_get_script_for_list(element, getter, param){
}
function default_ajax_options_for_submit(ajax_type, element_to_block) {
+ // the complete is not a function but an array so you can push other
+ // functions that will be executed after the ajax call completes
var options = {
type: ajax_type,
async: true,
context: element_to_block,
data: "_source_view=" + SOURCE_VIEW,
beforeSend: function() {
+ // console.debug('data: '+this.data);
if (this.context) {
$(this.context).block({
message: null
});
}
},
- complete:function() {
+ complete: [function() {
if (this.context) {
$(this.context).unblock();
}
enable_rich_interaction();
- },
+ }],
error: function(req, status) {
TracksPages.page_notify('error', i18n['common.ajaxError']+': '+status, 8);
}
@@ -829,17 +1037,21 @@ $(document).ajaxSend(function(event, request, settings) {
}
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
- request.setRequestHeader("X-CSRF-Token", $('meta[name=csrf-token]').attr('content'));
+ request.setRequestHeader('X-CSRF-Token', $('meta[name=csrf-token]').attr('content'));
request.setRequestHeader("Accept", "text/javascript");
});
function setup_periodic_check(url_for_check, interval_in_sec, method) {
- var ajaxMethod = (method ? method : "GET");
-
- function check_remote() {
- $.ajax(default_ajax_options_for_scripts(ajaxMethod, url_for_check, null));
- }
- setInterval(check_remote, interval_in_sec*1000);
+ setInterval(
+ function(){
+ var settings = default_ajax_options_for_scripts( method ? method : "GET", url_for_check, null);
+ if(typeof(AUTH_TOKEN) != 'undefined'){
+ settings.data += "&authenticity_token=" + encodeURIComponent( AUTH_TOKEN )
+ }
+ $.ajax(settings);
+ },
+ interval_in_sec*1000
+ );
}
function update_order(event, ui){
@@ -894,117 +1106,27 @@ function enable_rich_interaction(){
$('input.Date').datepicker({
'dateFormat': dateFormat,
'firstDay': weekStart,
- 'showAnim': 'fold'
+ 'showButtonPanel': true,
+ 'showWeek': true,
+ 'changeMonth': true,
+ 'changeYear': true,
+ 'maxDate': '+5y',
+ 'minDate': '-1y',
+ 'showAnim': '' /* leave empty, see #1117 */
});
/* Autocomplete */
- $('input[name=context_name]').autocomplete({
- source: relative_to_root('contexts.autocomplete'),
- selectFirst: true
- });
- $('input[name=project_name]').autocomplete({
- source: relative_to_root('projects.autocomplete'),
- selectFirst: true
- });
- $('input[name="project[default_context_name]"]').autocomplete({
- source: relative_to_root('contexts.autocomplete'),
- selectFirst: true
- });
-
- $('input[name=tag_list]:not(.ac_input)')
- .bind( "keydown", function( event ) { // don't navigate away from the field on tab when selecting an item
- if ( event.keyCode === $.ui.keyCode.TAB &&
- $( this ).data( "autocomplete" ).menu.active ) {
- event.preventDefault();
- }
- })
- .autocomplete({
- minLength: 0,
- source: function( request, response ) {
- var last_term = extractLast( request.term );
- if (last_term != "" && last_term != " ")
- $.ajax( {
- url: relative_to_root('tags.autocomplete'),
- dataType: 'json',
- data: {
- term: last_term
- },
- success: function(data, textStatus, jqXHR) {
- // remove spinner as removing the class is not always done by response
- $('input[name=tag_list]').removeClass('ui-autocomplete-loading');
- response(data, textStatus, jqXHR); // call jquery callback to handle data
- }
- })
- else {
- // remove spinner as typing will always add the spinner
- $('input[name=tag_list]').removeClass('ui-autocomplete-loading');
- }
- },
- focus: function() {
- // prevent value inserted on focus
- return false;
- },
- select: function( event, ui ) {
- var terms = split( this.value );
- // remove the current input
- terms.pop();
- // add the selected item
- terms.push( ui.item.value );
- // add placeholder to get the comma-and-space at the end
- //terms.push( "" );
- this.value = terms.join( ", " );
- return false;
- },
- selectFirst: true
- });
-
-
- function split( val ) {
- return val.split( /,\s*/ );
- }
- function extractLast( term ) {
- return split( term ).pop();
- }
-
-
- $('input[name=predecessor_list]:not(.ac_input)')
- .bind( "keydown", function( event ) { // don't navigate away from the field on tab when selecting an item
- if ( event.keyCode === $.ui.keyCode.TAB &&
- $( this ).data( "autocomplete" ).menu.active ) {
- event.preventDefault();
- }
- })
- .autocomplete({
- minLength: 0,
- source: function( request, response ) {
- var last_term = extractLast( request.term );
- if (last_term != "" && last_term != " ")
- $.getJSON( relative_to_root('auto_complete_for_predecessor'), {
- term: last_term
- }, response );
- },
- focus: function() {
- // prevent value inserted on focus
- return false;
- },
- select: function( event, ui ) {
- var terms = split( this.value );
- // remove the current input
- terms.pop();
- // add the selected item
- terms.push( ui.item.value );
- // add placeholder to get the comma-and-space at the end
- //terms.push( "" );
- this.value = terms.join( ", " );
- return false;
- }
- });
+ ProjectItems.setup_autocomplete_for_projects('input[name=project_name]');
+ ContextItems.setup_autocomplete_for_contexts('input[name=context_name]');
+ ContextItems.setup_autocomplete_for_contexts('input[id="project_default_context_name"]');
+ TracksPages.setup_autocomplete_for_tag_list('input[name=tag_list]');
+ TracksPages.setup_autocomplete_for_tag_list('input[id="project_default_tags"]');
+ TodoItems.setup_autocomplete_for_predecessor();
/* have to bind on keypress because of limitations of live() */
$('input[name=project_name]').live('keypress', function(){
$(this).bind('blur', project_defaults);
});
-
$('input[name=context_name]').live('keypress', function(){
$(this).attr('edited', 'true');
});
@@ -1018,15 +1140,10 @@ function enable_rich_interaction(){
var dropped_todo = this.id.split('_')[2];
ui.draggable.remove();
$('.drop_target').hide(); // IE8 doesn't call stop() in this situation
- $(this).block({
- message: null
- });
- $.post(relative_to_root('todos/add_predecessor'),
- {
- successor: dragged_todo,
- predecessor: dropped_todo
- },
- null, 'script');
+
+ ajax_options = default_ajax_options_for_scripts('POST', relative_to_root('todos/add_predecessor'), $(this));
+ ajax_options.data += "&predecessor="+dropped_todo + "&successor="+dragged_todo
+ $.ajax(ajax_options);
}
function drag_todo(){
@@ -1082,7 +1199,7 @@ function enable_rich_interaction(){
field_touched = false;
/* shrink the notes on the project pages. This is not live(), so this needs
-* to be run after ajax adding of a new note */
+ * to be run after ajax adding of a new note */
$('.note_wrapper').truncate({
max_length: 90,
more: '',
diff --git a/public/javascripts/i18n/jquery.ui.datepicker-de.js b/public/javascripts/i18n/jquery.ui.datepicker-de.js
new file mode 100644
index 00000000..ac2d516a
--- /dev/null
+++ b/public/javascripts/i18n/jquery.ui.datepicker-de.js
@@ -0,0 +1,23 @@
+/* German initialisation for the jQuery UI date picker plugin. */
+/* Written by Milian Wolff (mail@milianw.de). */
+jQuery(function($){
+ $.datepicker.regional['de'] = {
+ closeText: 'schließen',
+ prevText: '<zurück',
+ nextText: 'Vor>',
+ currentText: 'heute',
+ monthNames: ['Januar','Februar','März','April','Mai','Juni',
+ 'Juli','August','September','Oktober','November','Dezember'],
+ monthNamesShort: ['Jan','Feb','Mär','Apr','Mai','Jun',
+ 'Jul','Aug','Sep','Okt','Nov','Dez'],
+ dayNames: ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'],
+ dayNamesShort: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+ dayNamesMin: ['So','Mo','Di','Mi','Do','Fr','Sa'],
+ weekHeader: 'Wo',
+ dateFormat: 'dd.mm.yy',
+ firstDay: 1,
+ isRTL: false,
+ showMonthAfterYear: false,
+ yearSuffix: ''};
+ $.datepicker.setDefaults($.datepicker.regional['de']);
+});
diff --git a/public/javascripts/i18n/jquery.ui.datepicker-nl.js b/public/javascripts/i18n/jquery.ui.datepicker-nl.js
new file mode 100644
index 00000000..2d697f7e
--- /dev/null
+++ b/public/javascripts/i18n/jquery.ui.datepicker-nl.js
@@ -0,0 +1,23 @@
+/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */
+/* Written by Mathias Bynens */
+jQuery(function($){
+ $.datepicker.regional['nl'] = {
+ closeText: 'Sluiten',
+ prevText: '←',
+ nextText: '→',
+ currentText: 'Vandaag',
+ monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni',
+ 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+ monthNamesShort: ['jan', 'feb', 'maa', 'apr', 'mei', 'jun',
+ 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+ dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+ dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'],
+ dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+ weekHeader: 'Wk',
+ dateFormat: 'dd-mm-yy',
+ firstDay: 1,
+ isRTL: false,
+ showMonthAfterYear: false,
+ yearSuffix: ''};
+ $.datepicker.setDefaults($.datepicker.regional['nl']);
+});
\ No newline at end of file
diff --git a/public/javascripts/jquery-1.5.1.min.js b/public/javascripts/jquery-1.5.1.min.js
new file mode 100644
index 00000000..6437874c
--- /dev/null
+++ b/public/javascripts/jquery-1.5.1.min.js
@@ -0,0 +1,16 @@
+/*!
+ * jQuery JavaScript Library v1.5.1
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Wed Feb 23 13:55:29 2011 -0500
+ */
+(function(a,b){function cg(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cd(a){if(!bZ[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";bZ[a]=c}return bZ[a]}function cc(a,b){var c={};d.each(cb.concat.apply([],cb.slice(0,b)),function(){c[this]=a});return c}function bY(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bX(){try{return new a.XMLHttpRequest}catch(b){}}function bW(){d(a).unload(function(){for(var a in bU)bU[a](0,1)})}function bQ(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g=0===c})}function N(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function F(a,b){return(a&&a!=="*"?a+".":"")+b.replace(r,"`").replace(s,"&")}function E(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,q=[],r=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;ic)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function C(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function w(){return!0}function v(){return!1}function g(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function f(a,c,f){if(f===b&&a.nodeType===1){f=a.getAttribute("data-"+c);if(typeof f==="string"){try{f=f==="true"?!0:f==="false"?!1:f==="null"?null:d.isNaN(f)?e.test(f)?d.parseJSON(f):f:parseFloat(f)}catch(g){}d.data(a,c,f)}else f=b}return f}var c=a.document,d=function(){function I(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(I,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x=!1,y,z="then done fail isResolved isRejected promise".split(" "),A,B=Object.prototype.toString,C=Object.prototype.hasOwnProperty,D=Array.prototype.push,E=Array.prototype.slice,F=String.prototype.trim,G=Array.prototype.indexOf,H={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.1",length:0,size:function(){return this.length},toArray:function(){return E.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?D.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),y.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(E.apply(this,arguments),"slice",E.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:D,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;y.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=!0;if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",A,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",A),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&I()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):H[B.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!C.call(a,"constructor")&&!C.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||C.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g1){var f=E.call(arguments,0),g=b,h=function(a){return function(b){f[a]=arguments.length>1?E.call(arguments,0):b,--g||c.resolveWith(e,f)}};while(b--)a=f[b],a&&d.isFunction(a.promise)?a.promise().then(h(b),c.reject):--g;g||c.resolveWith(e,f)}else c!==a&&c.resolve(a);return e},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}d.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.subclass=this.subclass,a.fn.init=function b(b,c){c&&c instanceof d&&!(c instanceof a)&&(c=a(c));return d.fn.init.call(this,b,c,e)},a.fn.init.prototype=a.fn;var e=a(c);return a},browser:{}}),y=d._Deferred(),d.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){H["[object "+b+"]"]=b.toLowerCase()}),w=d.uaMatch(v),w.browser&&(d.browser[w.browser]=!0,d.browser.version=w.version),d.browser.webkit&&(d.browser.safari=!0),G&&(d.inArray=function(a,b){return G.call(b,a)}),i.test(" ")&&(j=/^[\s\xA0]+/,k=/[\s\xA0]+$/),g=d(c),c.addEventListener?A=function(){c.removeEventListener("DOMContentLoaded",A,!1),d.ready()}:c.attachEvent&&(A=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",A),d.ready())});return d}();(function(){d.support={};var b=c.createElement("div");b.style.display="none",b.innerHTML="
+
+
+ PAGE
+
+ attr_reader :files
+ attr_accessor :root, :path
+
+ def initialize(root, app=nil)
+ @root = F.expand_path(root)
+ @app = app || Rack::File.new(@root)
+ end
+
+ def call(env)
+ dup._call(env)
+ end
+
+ F = ::File
+
+ def _call(env)
+ @env = env
+ @script_name = env['SCRIPT_NAME']
+ @path_info = Utils.unescape(env['PATH_INFO'])
+
+ if forbidden = check_forbidden
+ forbidden
+ else
+ @path = F.join(@root, @path_info)
+ list_path
+ end
+ end
+
+ def check_forbidden
+ return unless @path_info.include? ".."
+
+ body = "Forbidden\n"
+ size = Rack::Utils.bytesize(body)
+ return [403, {"Content-Type" => "text/plain",
+ "Content-Length" => size.to_s,
+ "X-Cascade" => "pass"}, [body]]
+ end
+
+ def list_directory
+ @files = [['../','Parent Directory','','','']]
+ glob = F.join(@path, '*')
+
+ Dir[glob].sort.each do |node|
+ stat = stat(node)
+ next unless stat
+ basename = F.basename(node)
+ ext = F.extname(node)
+
+ url = F.join(@script_name, @path_info, basename)
+ size = stat.size
+ type = stat.directory? ? 'directory' : Mime.mime_type(ext)
+ size = stat.directory? ? '-' : filesize_format(size)
+ mtime = stat.mtime.httpdate
+ url << '/' if stat.directory?
+ basename << '/' if stat.directory?
+
+ @files << [ url, basename, size, type, mtime ]
+ end
+
+ return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
+ end
+
+ def stat(node, max = 10)
+ F.stat(node)
+ rescue Errno::ENOENT, Errno::ELOOP
+ return nil
+ end
+
+ # TODO: add correct response if not readable, not sure if 404 is the best
+ # option
+ def list_path
+ @stat = F.stat(@path)
+
+ if @stat.readable?
+ return @app.call(@env) if @stat.file?
+ return list_directory if @stat.directory?
+ else
+ raise Errno::ENOENT, 'No such file or directory'
+ end
+
+ rescue Errno::ENOENT, Errno::ELOOP
+ return entity_not_found
+ end
+
+ def entity_not_found
+ body = "Entity not found: #{@path_info}\n"
+ size = Rack::Utils.bytesize(body)
+ return [404, {"Content-Type" => "text/plain",
+ "Content-Length" => size.to_s,
+ "X-Cascade" => "pass"}, [body]]
+ end
+
+ def each
+ show_path = @path.sub(/^#{@root}/,'')
+ files = @files.map{|f| DIR_FILE % f }*"\n"
+ page = DIR_PAGE % [ show_path, show_path , files ]
+ page.each_line{|l| yield l }
+ end
+
+ # Stolen from Ramaze
+
+ FILESIZE_FORMAT = [
+ ['%.1fT', 1 << 40],
+ ['%.1fG', 1 << 30],
+ ['%.1fM', 1 << 20],
+ ['%.1fK', 1 << 10],
+ ]
+
+ def filesize_format(int)
+ FILESIZE_FORMAT.each do |format, size|
+ return format % (int.to_f / size) if int >= size
+ end
+
+ int.to_s + 'B'
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/etag.rb b/vendor/gems/rack-1.1.0/lib/rack/etag.rb
new file mode 100644
index 00000000..06dbc6aa
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/etag.rb
@@ -0,0 +1,23 @@
+require 'digest/md5'
+
+module Rack
+ # Automatically sets the ETag header on all String bodies
+ class ETag
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+
+ if !headers.has_key?('ETag')
+ parts = []
+ body.each { |part| parts << part.to_s }
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(parts.join(""))}")
+ [status, headers, parts]
+ else
+ [status, headers, body]
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/file.rb b/vendor/gems/rack-1.1.0/lib/rack/file.rb
new file mode 100644
index 00000000..14af7b3b
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/file.rb
@@ -0,0 +1,90 @@
+require 'time'
+require 'rack/utils'
+require 'rack/mime'
+
+module Rack
+ # Rack::File serves files below the +root+ given, according to the
+ # path info of the Rack request.
+ #
+ # Handlers can detect if bodies are a Rack::File, and use mechanisms
+ # like sendfile on the +path+.
+
+ class File
+ attr_accessor :root
+ attr_accessor :path
+
+ alias :to_path :path
+
+ def initialize(root)
+ @root = root
+ end
+
+ def call(env)
+ dup._call(env)
+ end
+
+ F = ::File
+
+ def _call(env)
+ @path_info = Utils.unescape(env["PATH_INFO"])
+ return forbidden if @path_info.include? ".."
+
+ @path = F.join(@root, @path_info)
+
+ begin
+ if F.file?(@path) && F.readable?(@path)
+ serving
+ else
+ raise Errno::EPERM
+ end
+ rescue SystemCallError
+ not_found
+ end
+ end
+
+ def forbidden
+ body = "Forbidden\n"
+ [403, {"Content-Type" => "text/plain",
+ "Content-Length" => body.size.to_s,
+ "X-Cascade" => "pass"},
+ [body]]
+ end
+
+ # NOTE:
+ # We check via File::size? whether this file provides size info
+ # via stat (e.g. /proc files often don't), otherwise we have to
+ # figure it out by reading the whole file into memory. And while
+ # we're at it we also use this as body then.
+
+ def serving
+ if size = F.size?(@path)
+ body = self
+ else
+ body = [F.read(@path)]
+ size = Utils.bytesize(body.first)
+ end
+
+ [200, {
+ "Last-Modified" => F.mtime(@path).httpdate,
+ "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
+ "Content-Length" => size.to_s
+ }, body]
+ end
+
+ def not_found
+ body = "File not found: #{@path_info}\n"
+ [404, {"Content-Type" => "text/plain",
+ "Content-Length" => body.size.to_s,
+ "X-Cascade" => "pass"},
+ [body]]
+ end
+
+ def each
+ F.open(@path, "rb") { |file|
+ while part = file.read(8192)
+ yield part
+ end
+ }
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler.rb b/vendor/gems/rack-1.1.0/lib/rack/handler.rb
new file mode 100644
index 00000000..3c09883e
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler.rb
@@ -0,0 +1,88 @@
+module Rack
+ # *Handlers* connect web servers with Rack.
+ #
+ # Rack includes Handlers for Mongrel, WEBrick, FastCGI, CGI, SCGI
+ # and LiteSpeed.
+ #
+ # Handlers usually are activated by calling MyHandler.run(myapp).
+ # A second optional hash can be passed to include server-specific
+ # configuration.
+ module Handler
+ def self.get(server)
+ return unless server
+ server = server.to_s
+
+ if klass = @handlers[server]
+ obj = Object
+ klass.split("::").each { |x| obj = obj.const_get(x) }
+ obj
+ else
+ try_require('rack/handler', server)
+ const_get(server)
+ end
+ end
+
+ def self.default(options = {})
+ # Guess.
+ if ENV.include?("PHP_FCGI_CHILDREN")
+ # We already speak FastCGI
+ options.delete :File
+ options.delete :Port
+
+ Rack::Handler::FastCGI
+ elsif ENV.include?("REQUEST_METHOD")
+ Rack::Handler::CGI
+ else
+ begin
+ Rack::Handler::Mongrel
+ rescue LoadError => e
+ Rack::Handler::WEBrick
+ end
+ end
+ end
+
+ # Transforms server-name constants to their canonical form as filenames,
+ # then tries to require them but silences the LoadError if not found
+ #
+ # Naming convention:
+ #
+ # Foo # => 'foo'
+ # FooBar # => 'foo_bar.rb'
+ # FooBAR # => 'foobar.rb'
+ # FOObar # => 'foobar.rb'
+ # FOOBAR # => 'foobar.rb'
+ # FooBarBaz # => 'foo_bar_baz.rb'
+ def self.try_require(prefix, const_name)
+ file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
+ gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
+
+ require(::File.join(prefix, file))
+ rescue LoadError
+ end
+
+ def self.register(server, klass)
+ @handlers ||= {}
+ @handlers[server] = klass
+ end
+
+ autoload :CGI, "rack/handler/cgi"
+ autoload :FastCGI, "rack/handler/fastcgi"
+ autoload :Mongrel, "rack/handler/mongrel"
+ autoload :EventedMongrel, "rack/handler/evented_mongrel"
+ autoload :SwiftipliedMongrel, "rack/handler/swiftiplied_mongrel"
+ autoload :WEBrick, "rack/handler/webrick"
+ autoload :LSWS, "rack/handler/lsws"
+ autoload :SCGI, "rack/handler/scgi"
+ autoload :Thin, "rack/handler/thin"
+
+ register 'cgi', 'Rack::Handler::CGI'
+ register 'fastcgi', 'Rack::Handler::FastCGI'
+ register 'mongrel', 'Rack::Handler::Mongrel'
+ register 'emongrel', 'Rack::Handler::EventedMongrel'
+ register 'smongrel', 'Rack::Handler::SwiftipliedMongrel'
+ register 'webrick', 'Rack::Handler::WEBrick'
+ register 'lsws', 'Rack::Handler::LSWS'
+ register 'scgi', 'Rack::Handler::SCGI'
+ register 'thin', 'Rack::Handler::Thin'
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/cgi.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/cgi.rb
new file mode 100644
index 00000000..c6903f15
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/cgi.rb
@@ -0,0 +1,61 @@
+require 'rack/content_length'
+
+module Rack
+ module Handler
+ class CGI
+ def self.run(app, options=nil)
+ serve app
+ end
+
+ def self.serve(app)
+ app = ContentLength.new(app)
+
+ env = ENV.to_hash
+ env.delete "HTTP_CONTENT_LENGTH"
+
+ env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
+
+ env.update({"rack.version" => [1,1],
+ "rack.input" => $stdin,
+ "rack.errors" => $stderr,
+
+ "rack.multithread" => false,
+ "rack.multiprocess" => true,
+ "rack.run_once" => true,
+
+ "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
+ })
+
+ env["QUERY_STRING"] ||= ""
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
+ env["REQUEST_PATH"] ||= "/"
+
+ status, headers, body = app.call(env)
+ begin
+ send_headers status, headers
+ send_body body
+ ensure
+ body.close if body.respond_to? :close
+ end
+ end
+
+ def self.send_headers(status, headers)
+ STDOUT.print "Status: #{status}\r\n"
+ headers.each { |k, vs|
+ vs.split("\n").each { |v|
+ STDOUT.print "#{k}: #{v}\r\n"
+ }
+ }
+ STDOUT.print "\r\n"
+ STDOUT.flush
+ end
+
+ def self.send_body(body)
+ body.each { |part|
+ STDOUT.print part
+ STDOUT.flush
+ }
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/evented_mongrel.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/evented_mongrel.rb
new file mode 100644
index 00000000..0f5cbf72
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/evented_mongrel.rb
@@ -0,0 +1,8 @@
+require 'swiftcore/evented_mongrel'
+
+module Rack
+ module Handler
+ class EventedMongrel < Handler::Mongrel
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/fastcgi.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/fastcgi.rb
new file mode 100644
index 00000000..b992a5f4
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/fastcgi.rb
@@ -0,0 +1,89 @@
+require 'fcgi'
+require 'socket'
+require 'rack/content_length'
+require 'rack/rewindable_input'
+
+if defined? FCGI::Stream
+ class FCGI::Stream
+ alias _rack_read_without_buffer read
+
+ def read(n, buffer=nil)
+ buf = _rack_read_without_buffer n
+ buffer.replace(buf.to_s) if buffer
+ buf
+ end
+ end
+end
+
+module Rack
+ module Handler
+ class FastCGI
+ def self.run(app, options={})
+ file = options[:File] and STDIN.reopen(UNIXServer.new(file))
+ port = options[:Port] and STDIN.reopen(TCPServer.new(port))
+ FCGI.each { |request|
+ serve request, app
+ }
+ end
+
+ def self.serve(request, app)
+ app = Rack::ContentLength.new(app)
+
+ env = request.env
+ env.delete "HTTP_CONTENT_LENGTH"
+
+ env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
+
+ rack_input = RewindableInput.new(request.in)
+
+ env.update({"rack.version" => [1,1],
+ "rack.input" => rack_input,
+ "rack.errors" => request.err,
+
+ "rack.multithread" => false,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
+ })
+
+ env["QUERY_STRING"] ||= ""
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
+ env["REQUEST_PATH"] ||= "/"
+ env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
+ env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""
+
+ begin
+ status, headers, body = app.call(env)
+ begin
+ send_headers request.out, status, headers
+ send_body request.out, body
+ ensure
+ body.close if body.respond_to? :close
+ end
+ ensure
+ rack_input.close
+ request.finish
+ end
+ end
+
+ def self.send_headers(out, status, headers)
+ out.print "Status: #{status}\r\n"
+ headers.each { |k, vs|
+ vs.split("\n").each { |v|
+ out.print "#{k}: #{v}\r\n"
+ }
+ }
+ out.print "\r\n"
+ out.flush
+ end
+
+ def self.send_body(out, body)
+ body.each { |part|
+ out.print part
+ out.flush
+ }
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/lsws.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/lsws.rb
new file mode 100644
index 00000000..eabc0bc9
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/lsws.rb
@@ -0,0 +1,63 @@
+require 'lsapi'
+require 'rack/content_length'
+require 'rack/rewindable_input'
+
+module Rack
+ module Handler
+ class LSWS
+ def self.run(app, options=nil)
+ while LSAPI.accept != nil
+ serve app
+ end
+ end
+ def self.serve(app)
+ app = Rack::ContentLength.new(app)
+
+ env = ENV.to_hash
+ env.delete "HTTP_CONTENT_LENGTH"
+ env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
+
+ rack_input = RewindableInput.new($stdin.read.to_s)
+
+ env.update(
+ "rack.version" => [1,1],
+ "rack.input" => rack_input,
+ "rack.errors" => $stderr,
+ "rack.multithread" => false,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+ "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
+ )
+
+ env["QUERY_STRING"] ||= ""
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
+ env["REQUEST_PATH"] ||= "/"
+ status, headers, body = app.call(env)
+ begin
+ send_headers status, headers
+ send_body body
+ ensure
+ body.close if body.respond_to? :close
+ end
+ ensure
+ rack_input.close
+ end
+ def self.send_headers(status, headers)
+ print "Status: #{status}\r\n"
+ headers.each { |k, vs|
+ vs.split("\n").each { |v|
+ print "#{k}: #{v}\r\n"
+ }
+ }
+ print "\r\n"
+ STDOUT.flush
+ end
+ def self.send_body(body)
+ body.each { |part|
+ print part
+ STDOUT.flush
+ }
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/mongrel.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/mongrel.rb
new file mode 100644
index 00000000..b6b775ea
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/mongrel.rb
@@ -0,0 +1,90 @@
+require 'mongrel'
+require 'stringio'
+require 'rack/content_length'
+require 'rack/chunked'
+
+module Rack
+ module Handler
+ class Mongrel < ::Mongrel::HttpHandler
+ def self.run(app, options={})
+ server = ::Mongrel::HttpServer.new(
+ options[:Host] || '0.0.0.0',
+ options[:Port] || 8080,
+ options[:num_processors] || 950,
+ options[:throttle] || 0,
+ options[:timeout] || 60)
+ # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
+ # Use is similar to #run, replacing the app argument with a hash of
+ # { path=>app, ... } or an instance of Rack::URLMap.
+ if options[:map]
+ if app.is_a? Hash
+ app.each do |path, appl|
+ path = '/'+path unless path[0] == ?/
+ server.register(path, Rack::Handler::Mongrel.new(appl))
+ end
+ elsif app.is_a? URLMap
+ app.instance_variable_get(:@mapping).each do |(host, path, appl)|
+ next if !host.nil? && !options[:Host].nil? && options[:Host] != host
+ path = '/'+path unless path[0] == ?/
+ server.register(path, Rack::Handler::Mongrel.new(appl))
+ end
+ else
+ raise ArgumentError, "first argument should be a Hash or URLMap"
+ end
+ else
+ server.register('/', Rack::Handler::Mongrel.new(app))
+ end
+ yield server if block_given?
+ server.run.join
+ end
+
+ def initialize(app)
+ @app = Rack::Chunked.new(Rack::ContentLength.new(app))
+ end
+
+ def process(request, response)
+ env = {}.replace(request.params)
+ env.delete "HTTP_CONTENT_TYPE"
+ env.delete "HTTP_CONTENT_LENGTH"
+
+ env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
+
+ rack_input = request.body || StringIO.new('')
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
+ env.update({"rack.version" => [1,1],
+ "rack.input" => rack_input,
+ "rack.errors" => $stderr,
+
+ "rack.multithread" => true,
+ "rack.multiprocess" => false, # ???
+ "rack.run_once" => false,
+
+ "rack.url_scheme" => "http",
+ })
+ env["QUERY_STRING"] ||= ""
+
+ status, headers, body = @app.call(env)
+
+ begin
+ response.status = status.to_i
+ response.send_status(nil)
+
+ headers.each { |k, vs|
+ vs.split("\n").each { |v|
+ response.header[k] = v
+ }
+ }
+ response.send_header
+
+ body.each { |part|
+ response.write part
+ response.socket.flush
+ }
+ ensure
+ body.close if body.respond_to? :close
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/scgi.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/scgi.rb
new file mode 100644
index 00000000..79a6b2bd
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/scgi.rb
@@ -0,0 +1,62 @@
+require 'scgi'
+require 'stringio'
+require 'rack/content_length'
+require 'rack/chunked'
+
+module Rack
+ module Handler
+ class SCGI < ::SCGI::Processor
+ attr_accessor :app
+
+ def self.run(app, options=nil)
+ new(options.merge(:app=>app,
+ :host=>options[:Host],
+ :port=>options[:Port],
+ :socket=>options[:Socket])).listen
+ end
+
+ def initialize(settings = {})
+ @app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
+ @log = Object.new
+ def @log.info(*args); end
+ def @log.error(*args); end
+ super(settings)
+ end
+
+ def process_request(request, input_body, socket)
+ env = {}.replace(request)
+ env.delete "HTTP_CONTENT_TYPE"
+ env.delete "HTTP_CONTENT_LENGTH"
+ env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2)
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
+ env["PATH_INFO"] = env["REQUEST_PATH"]
+ env["QUERY_STRING"] ||= ""
+ env["SCRIPT_NAME"] = ""
+
+ rack_input = StringIO.new(input_body)
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
+ env.update({"rack.version" => [1,1],
+ "rack.input" => rack_input,
+ "rack.errors" => $stderr,
+ "rack.multithread" => true,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+
+ "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
+ })
+ status, headers, body = app.call(env)
+ begin
+ socket.write("Status: #{status}\r\n")
+ headers.each do |k, vs|
+ vs.split("\n").each { |v| socket.write("#{k}: #{v}\r\n")}
+ end
+ socket.write("\r\n")
+ body.each {|s| socket.write(s)}
+ ensure
+ body.close if body.respond_to? :close
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/swiftiplied_mongrel.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/swiftiplied_mongrel.rb
new file mode 100644
index 00000000..4bafd0b9
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/swiftiplied_mongrel.rb
@@ -0,0 +1,8 @@
+require 'swiftcore/swiftiplied_mongrel'
+
+module Rack
+ module Handler
+ class SwiftipliedMongrel < Handler::Mongrel
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/thin.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/thin.rb
new file mode 100644
index 00000000..3d4fedff
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/thin.rb
@@ -0,0 +1,18 @@
+require "thin"
+require "rack/content_length"
+require "rack/chunked"
+
+module Rack
+ module Handler
+ class Thin
+ def self.run(app, options={})
+ app = Rack::Chunked.new(Rack::ContentLength.new(app))
+ server = ::Thin::Server.new(options[:Host] || '0.0.0.0',
+ options[:Port] || 8080,
+ app)
+ yield server if block_given?
+ server.start
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/handler/webrick.rb b/vendor/gems/rack-1.1.0/lib/rack/handler/webrick.rb
new file mode 100644
index 00000000..8d7f5724
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/handler/webrick.rb
@@ -0,0 +1,69 @@
+require 'webrick'
+require 'stringio'
+require 'rack/content_length'
+
+module Rack
+ module Handler
+ class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
+ def self.run(app, options={})
+ options[:BindAddress] = options.delete(:Host) if options[:Host]
+ server = ::WEBrick::HTTPServer.new(options)
+ server.mount "/", Rack::Handler::WEBrick, app
+ trap(:INT) { server.shutdown }
+ yield server if block_given?
+ server.start
+ end
+
+ def initialize(server, app)
+ super server
+ @app = Rack::ContentLength.new(app)
+ end
+
+ def service(req, res)
+ env = req.meta_vars
+ env.delete_if { |k, v| v.nil? }
+
+ rack_input = StringIO.new(req.body.to_s)
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
+ env.update({"rack.version" => [1,1],
+ "rack.input" => rack_input,
+ "rack.errors" => $stderr,
+
+ "rack.multithread" => true,
+ "rack.multiprocess" => false,
+ "rack.run_once" => false,
+
+ "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
+ })
+
+ env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
+ env["QUERY_STRING"] ||= ""
+ env["REQUEST_PATH"] ||= "/"
+ unless env["PATH_INFO"] == ""
+ path, n = req.request_uri.path, env["SCRIPT_NAME"].length
+ env["PATH_INFO"] = path[n, path.length-n]
+ end
+
+ status, headers, body = @app.call(env)
+ begin
+ res.status = status.to_i
+ headers.each { |k, vs|
+ if k.downcase == "set-cookie"
+ res.cookies.concat vs.split("\n")
+ else
+ vs.split("\n").each { |v|
+ res[k] = v
+ }
+ end
+ }
+ body.each { |part|
+ res.body << part
+ }
+ ensure
+ body.close if body.respond_to? :close
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/head.rb b/vendor/gems/rack-1.1.0/lib/rack/head.rb
new file mode 100644
index 00000000..deab822a
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/head.rb
@@ -0,0 +1,19 @@
+module Rack
+
+class Head
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+
+ if env["REQUEST_METHOD"] == "HEAD"
+ [status, headers, []]
+ else
+ [status, headers, body]
+ end
+ end
+end
+
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/lint.rb b/vendor/gems/rack-1.1.0/lib/rack/lint.rb
new file mode 100644
index 00000000..534375b9
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/lint.rb
@@ -0,0 +1,575 @@
+require 'rack/utils'
+
+module Rack
+ # Rack::Lint validates your application and the requests and
+ # responses according to the Rack spec.
+
+ class Lint
+ def initialize(app)
+ @app = app
+ end
+
+ # :stopdoc:
+
+ class LintError < RuntimeError; end
+ module Assertion
+ def assert(message, &block)
+ unless block.call
+ raise LintError, message
+ end
+ end
+ end
+ include Assertion
+
+ ## This specification aims to formalize the Rack protocol. You
+ ## can (and should) use Rack::Lint to enforce it.
+ ##
+ ## When you develop middleware, be sure to add a Lint before and
+ ## after to catch all mistakes.
+
+ ## = Rack applications
+
+ ## A Rack application is an Ruby object (not a class) that
+ ## responds to +call+.
+ def call(env=nil)
+ dup._call(env)
+ end
+
+ def _call(env)
+ ## It takes exactly one argument, the *environment*
+ assert("No env given") { env }
+ check_env env
+
+ env['rack.input'] = InputWrapper.new(env['rack.input'])
+ env['rack.errors'] = ErrorWrapper.new(env['rack.errors'])
+
+ ## and returns an Array of exactly three values:
+ status, headers, @body = @app.call(env)
+ ## The *status*,
+ check_status status
+ ## the *headers*,
+ check_headers headers
+ ## and the *body*.
+ check_content_type status, headers
+ check_content_length status, headers, env
+ [status, headers, self]
+ end
+
+ ## == The Environment
+ def check_env(env)
+ ## The environment must be an true instance of Hash (no
+ ## subclassing allowed) that includes CGI-like headers.
+ ## The application is free to modify the environment.
+ assert("env #{env.inspect} is not a Hash, but #{env.class}") {
+ env.kind_of? Hash
+ }
+
+ ##
+ ## The environment is required to include these variables
+ ## (adopted from PEP333), except when they'd be empty, but see
+ ## below.
+
+ ## REQUEST_METHOD:: The HTTP request method, such as
+ ## "GET" or "POST". This cannot ever
+ ## be an empty string, and so is
+ ## always required.
+
+ ## SCRIPT_NAME:: The initial portion of the request
+ ## URL's "path" that corresponds to the
+ ## application object, so that the
+ ## application knows its virtual
+ ## "location". This may be an empty
+ ## string, if the application corresponds
+ ## to the "root" of the server.
+
+ ## PATH_INFO:: The remainder of the request URL's
+ ## "path", designating the virtual
+ ## "location" of the request's target
+ ## within the application. This may be an
+ ## empty string, if the request URL targets
+ ## the application root and does not have a
+ ## trailing slash. This value may be
+ ## percent-encoded when I originating from
+ ## a URL.
+
+ ## QUERY_STRING:: The portion of the request URL that
+ ## follows the ?, if any. May be
+ ## empty, but is always required!
+
+ ## SERVER_NAME, SERVER_PORT:: When combined with SCRIPT_NAME and PATH_INFO, these variables can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT can never be empty strings, and so are always required.
+
+ ## HTTP_ Variables:: Variables corresponding to the
+ ## client-supplied HTTP request
+ ## headers (i.e., variables whose
+ ## names begin with HTTP_). The
+ ## presence or absence of these
+ ## variables should correspond with
+ ## the presence or absence of the
+ ## appropriate HTTP header in the
+ ## request.
+
+ ## In addition to this, the Rack environment must include these
+ ## Rack-specific variables:
+
+ ## rack.version:: The Array [1,1], representing this version of Rack.
+ ## rack.url_scheme:: +http+ or +https+, depending on the request URL.
+ ## rack.input:: See below, the input stream.
+ ## rack.errors:: See below, the error stream.
+ ## rack.multithread:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
+ ## rack.multiprocess:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
+ ## rack.run_once:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
+ ##
+
+ ## Additional environment specifications have approved to
+ ## standardized middleware APIs. None of these are required to
+ ## be implemented by the server.
+
+ ## rack.session:: A hash like interface for storing request session data.
+ ## The store must implement:
+ if session = env['rack.session']
+ ## store(key, value) (aliased as []=);
+ assert("session #{session.inspect} must respond to store and []=") {
+ session.respond_to?(:store) && session.respond_to?(:[]=)
+ }
+
+ ## fetch(key, default = nil) (aliased as []);
+ assert("session #{session.inspect} must respond to fetch and []") {
+ session.respond_to?(:fetch) && session.respond_to?(:[])
+ }
+
+ ## delete(key);
+ assert("session #{session.inspect} must respond to delete") {
+ session.respond_to?(:delete)
+ }
+
+ ## clear;
+ assert("session #{session.inspect} must respond to clear") {
+ session.respond_to?(:clear)
+ }
+ end
+
+ ## rack.logger:: A common object interface for logging messages.
+ ## The object must implement:
+ if logger = env['rack.logger']
+ ## info(message, &block)
+ assert("logger #{logger.inspect} must respond to info") {
+ logger.respond_to?(:info)
+ }
+
+ ## debug(message, &block)
+ assert("logger #{logger.inspect} must respond to debug") {
+ logger.respond_to?(:debug)
+ }
+
+ ## warn(message, &block)
+ assert("logger #{logger.inspect} must respond to warn") {
+ logger.respond_to?(:warn)
+ }
+
+ ## error(message, &block)
+ assert("logger #{logger.inspect} must respond to error") {
+ logger.respond_to?(:error)
+ }
+
+ ## fatal(message, &block)
+ assert("logger #{logger.inspect} must respond to fatal") {
+ logger.respond_to?(:fatal)
+ }
+ end
+
+ ## The server or the application can store their own data in the
+ ## environment, too. The keys must contain at least one dot,
+ ## and should be prefixed uniquely. The prefix rack.
+ ## is reserved for use with the Rack core distribution and other
+ ## accepted specifications and must not be used otherwise.
+ ##
+
+ %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
+ QUERY_STRING
+ rack.version rack.input rack.errors
+ rack.multithread rack.multiprocess rack.run_once].each { |header|
+ assert("env missing required key #{header}") { env.include? header }
+ }
+
+ ## The environment must not contain the keys
+ ## HTTP_CONTENT_TYPE or HTTP_CONTENT_LENGTH
+ ## (use the versions without HTTP_).
+ %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
+ assert("env contains #{header}, must use #{header[5,-1]}") {
+ not env.include? header
+ }
+ }
+
+ ## The CGI keys (named without a period) must have String values.
+ env.each { |key, value|
+ next if key.include? "." # Skip extensions
+ assert("env variable #{key} has non-string value #{value.inspect}") {
+ value.kind_of? String
+ }
+ }
+
+ ##
+ ## There are the following restrictions:
+
+ ## * rack.version must be an array of Integers.
+ assert("rack.version must be an Array, was #{env["rack.version"].class}") {
+ env["rack.version"].kind_of? Array
+ }
+ ## * rack.url_scheme must either be +http+ or +https+.
+ assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
+ %w[http https].include? env["rack.url_scheme"]
+ }
+
+ ## * There must be a valid input stream in rack.input.
+ check_input env["rack.input"]
+ ## * There must be a valid error stream in rack.errors.
+ check_error env["rack.errors"]
+
+ ## * The REQUEST_METHOD must be a valid token.
+ assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
+ env["REQUEST_METHOD"] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
+ }
+
+ ## * The SCRIPT_NAME, if non-empty, must start with /
+ assert("SCRIPT_NAME must start with /") {
+ !env.include?("SCRIPT_NAME") ||
+ env["SCRIPT_NAME"] == "" ||
+ env["SCRIPT_NAME"] =~ /\A\//
+ }
+ ## * The PATH_INFO, if non-empty, must start with /
+ assert("PATH_INFO must start with /") {
+ !env.include?("PATH_INFO") ||
+ env["PATH_INFO"] == "" ||
+ env["PATH_INFO"] =~ /\A\//
+ }
+ ## * The CONTENT_LENGTH, if given, must consist of digits only.
+ assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
+ !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
+ }
+
+ ## * One of SCRIPT_NAME or PATH_INFO must be
+ ## set. PATH_INFO should be / if
+ ## SCRIPT_NAME is empty.
+ assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
+ env["SCRIPT_NAME"] || env["PATH_INFO"]
+ }
+ ## SCRIPT_NAME never should be /, but instead be empty.
+ assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
+ env["SCRIPT_NAME"] != "/"
+ }
+ end
+
+ ## === The Input Stream
+ ##
+ ## The input stream is an IO-like object which contains the raw HTTP
+ ## POST data.
+ def check_input(input)
+ ## When applicable, its external encoding must be "ASCII-8BIT" and it
+ ## must be opened in binary mode, for Ruby 1.9 compatibility.
+ assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
+ input.external_encoding.name == "ASCII-8BIT"
+ } if input.respond_to?(:external_encoding)
+ assert("rack.input #{input} is not opened in binary mode") {
+ input.binmode?
+ } if input.respond_to?(:binmode?)
+
+ ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
+ [:gets, :each, :read, :rewind].each { |method|
+ assert("rack.input #{input} does not respond to ##{method}") {
+ input.respond_to? method
+ }
+ }
+ end
+
+ class InputWrapper
+ include Assertion
+
+ def initialize(input)
+ @input = input
+ end
+
+ def size
+ @input.size
+ end
+
+ ## * +gets+ must be called without arguments and return a string,
+ ## or +nil+ on EOF.
+ def gets(*args)
+ assert("rack.input#gets called with arguments") { args.size == 0 }
+ v = @input.gets
+ assert("rack.input#gets didn't return a String") {
+ v.nil? or v.kind_of? String
+ }
+ v
+ end
+
+ ## * +read+ behaves like IO#read. Its signature is read([length, [buffer]]).
+ ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
+ ## be a String and may not be nil. If +length+ is given and not nil, then this method
+ ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
+ ## then this method reads all data until EOF.
+ ## When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
+ ## if +length+ is not given or is nil.
+ ## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
+ ## newly created String object.
+ def read(*args)
+ assert("rack.input#read called with too many arguments") {
+ args.size <= 2
+ }
+ if args.size >= 1
+ assert("rack.input#read called with non-integer and non-nil length") {
+ args.first.kind_of?(Integer) || args.first.nil?
+ }
+ assert("rack.input#read called with a negative length") {
+ args.first.nil? || args.first >= 0
+ }
+ end
+ if args.size >= 2
+ assert("rack.input#read called with non-String buffer") {
+ args[1].kind_of?(String)
+ }
+ end
+
+ v = @input.read(*args)
+
+ assert("rack.input#read didn't return nil or a String") {
+ v.nil? or v.kind_of? String
+ }
+ if args[0].nil?
+ assert("rack.input#read(nil) returned nil on EOF") {
+ !v.nil?
+ }
+ end
+
+ v
+ end
+
+ ## * +each+ must be called without arguments and only yield Strings.
+ def each(*args)
+ assert("rack.input#each called with arguments") { args.size == 0 }
+ @input.each { |line|
+ assert("rack.input#each didn't yield a String") {
+ line.kind_of? String
+ }
+ yield line
+ }
+ end
+
+ ## * +rewind+ must be called without arguments. It rewinds the input
+ ## stream back to the beginning. It must not raise Errno::ESPIPE:
+ ## that is, it may not be a pipe or a socket. Therefore, handler
+ ## developers must buffer the input data into some rewindable object
+ ## if the underlying input stream is not rewindable.
+ def rewind(*args)
+ assert("rack.input#rewind called with arguments") { args.size == 0 }
+ assert("rack.input#rewind raised Errno::ESPIPE") {
+ begin
+ @input.rewind
+ true
+ rescue Errno::ESPIPE
+ false
+ end
+ }
+ end
+
+ ## * +close+ must never be called on the input stream.
+ def close(*args)
+ assert("rack.input#close must not be called") { false }
+ end
+ end
+
+ ## === The Error Stream
+ def check_error(error)
+ ## The error stream must respond to +puts+, +write+ and +flush+.
+ [:puts, :write, :flush].each { |method|
+ assert("rack.error #{error} does not respond to ##{method}") {
+ error.respond_to? method
+ }
+ }
+ end
+
+ class ErrorWrapper
+ include Assertion
+
+ def initialize(error)
+ @error = error
+ end
+
+ ## * +puts+ must be called with a single argument that responds to +to_s+.
+ def puts(str)
+ @error.puts str
+ end
+
+ ## * +write+ must be called with a single argument that is a String.
+ def write(str)
+ assert("rack.errors#write not called with a String") { str.kind_of? String }
+ @error.write str
+ end
+
+ ## * +flush+ must be called without arguments and must be called
+ ## in order to make the error appear for sure.
+ def flush
+ @error.flush
+ end
+
+ ## * +close+ must never be called on the error stream.
+ def close(*args)
+ assert("rack.errors#close must not be called") { false }
+ end
+ end
+
+ ## == The Response
+
+ ## === The Status
+ def check_status(status)
+ ## This is an HTTP status. When parsed as integer (+to_i+), it must be
+ ## greater than or equal to 100.
+ assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
+ end
+
+ ## === The Headers
+ def check_headers(header)
+ ## The header must respond to +each+, and yield values of key and value.
+ assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
+ header.respond_to? :each
+ }
+ header.each { |key, value|
+ ## The header keys must be Strings.
+ assert("header key must be a string, was #{key.class}") {
+ key.kind_of? String
+ }
+ ## The header must not contain a +Status+ key,
+ assert("header must not contain Status") { key.downcase != "status" }
+ ## contain keys with : or newlines in their name,
+ assert("header names must not contain : or \\n") { key !~ /[:\n]/ }
+ ## contain keys names that end in - or _,
+ assert("header names must not end in - or _") { key !~ /[-_]\z/ }
+ ## but only contain keys that consist of
+ ## letters, digits, _ or - and start with a letter.
+ assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
+
+ ## The values of the header must be Strings,
+ assert("a header value must be a String, but the value of " +
+ "'#{key}' is a #{value.class}") { value.kind_of? String }
+ ## consisting of lines (for multiple header values, e.g. multiple
+ ## Set-Cookie values) seperated by "\n".
+ value.split("\n").each { |item|
+ ## The lines must not contain characters below 037.
+ assert("invalid header value #{key}: #{item.inspect}") {
+ item !~ /[\000-\037]/
+ }
+ }
+ }
+ end
+
+ ## === The Content-Type
+ def check_content_type(status, headers)
+ headers.each { |key, value|
+ ## There must be a Content-Type, except when the
+ ## +Status+ is 1xx, 204 or 304, in which case there must be none
+ ## given.
+ if key.downcase == "content-type"
+ assert("Content-Type header found in #{status} response, not allowed") {
+ not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+ return
+ end
+ }
+ assert("No Content-Type header found") {
+ Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+ end
+
+ ## === The Content-Length
+ def check_content_length(status, headers, env)
+ headers.each { |key, value|
+ if key.downcase == 'content-length'
+ ## There must not be a Content-Length header when the
+ ## +Status+ is 1xx, 204 or 304.
+ assert("Content-Length header found in #{status} response, not allowed") {
+ not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+
+ bytes = 0
+ string_body = true
+
+ if @body.respond_to?(:to_ary)
+ @body.each { |part|
+ unless part.kind_of?(String)
+ string_body = false
+ break
+ end
+
+ bytes += Rack::Utils.bytesize(part)
+ }
+
+ if env["REQUEST_METHOD"] == "HEAD"
+ assert("Response body was given for HEAD request, but should be empty") {
+ bytes == 0
+ }
+ else
+ if string_body
+ assert("Content-Length header was #{value}, but should be #{bytes}") {
+ value == bytes.to_s
+ }
+ end
+ end
+ end
+
+ return
+ end
+ }
+ end
+
+ ## === The Body
+ def each
+ @closed = false
+ ## The Body must respond to +each+
+ @body.each { |part|
+ ## and must only yield String values.
+ assert("Body yielded non-string value #{part.inspect}") {
+ part.kind_of? String
+ }
+ yield part
+ }
+ ##
+ ## The Body itself should not be an instance of String, as this will
+ ## break in Ruby 1.9.
+ ##
+ ## If the Body responds to +close+, it will be called after iteration.
+ # XXX howto: assert("Body has not been closed") { @closed }
+
+
+ ##
+ ## If the Body responds to +to_path+, it must return a String
+ ## identifying the location of a file whose contents are identical
+ ## to that produced by calling +each+; this may be used by the
+ ## server as an alternative, possibly more efficient way to
+ ## transport the response.
+
+ if @body.respond_to?(:to_path)
+ assert("The file identified by body.to_path does not exist") {
+ ::File.exist? @body.to_path
+ }
+ end
+
+ ##
+ ## The Body commonly is an Array of Strings, the application
+ ## instance itself, or a File-like object.
+ end
+
+ def close
+ @closed = true
+ @body.close if @body.respond_to?(:close)
+ end
+
+ # :startdoc:
+
+ end
+end
+
+## == Thanks
+## Some parts of this specification are adopted from PEP333: Python
+## Web Server Gateway Interface
+## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
+## everyone involved in that effort.
diff --git a/vendor/gems/rack-1.1.0/lib/rack/lobster.rb b/vendor/gems/rack-1.1.0/lib/rack/lobster.rb
new file mode 100644
index 00000000..f63f419a
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/lobster.rb
@@ -0,0 +1,65 @@
+require 'zlib'
+
+require 'rack/request'
+require 'rack/response'
+
+module Rack
+ # Paste has a Pony, Rack has a Lobster!
+ class Lobster
+ LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2
+ P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0
+ t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ
+ I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0])
+
+ LambdaLobster = lambda { |env|
+ if env["QUERY_STRING"].include?("flip")
+ lobster = LobsterString.split("\n").
+ map { |line| line.ljust(42).reverse }.
+ join("\n")
+ href = "?"
+ else
+ lobster = LobsterString
+ href = "?flip"
+ end
+
+ content = ["Lobstericious!",
+ "
+ You're seeing this error because you use Rack::ShowExceptions.
+
+
+
+
+
+HTML
+
+ # :startdoc:
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/showstatus.rb b/vendor/gems/rack-1.1.0/lib/rack/showstatus.rb
new file mode 100644
index 00000000..28258c7c
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/showstatus.rb
@@ -0,0 +1,106 @@
+require 'erb'
+require 'rack/request'
+require 'rack/utils'
+
+module Rack
+ # Rack::ShowStatus catches all empty responses the app it wraps and
+ # replaces them with a site explaining the error.
+ #
+ # Additional details can be put into rack.showstatus.detail
+ # and will be shown as HTML. If such details exist, the error page
+ # is always rendered, even if the reply was not empty.
+
+ class ShowStatus
+ def initialize(app)
+ @app = app
+ @template = ERB.new(TEMPLATE)
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+ headers = Utils::HeaderHash.new(headers)
+ empty = headers['Content-Length'].to_i <= 0
+
+ # client or server error, or explicit message
+ if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
+ req = Rack::Request.new(env)
+ message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
+ detail = env["rack.showstatus.detail"] || message
+ body = @template.result(binding)
+ size = Rack::Utils.bytesize(body)
+ [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]]
+ else
+ [status, headers, body]
+ end
+ end
+
+ def h(obj) # :nodoc:
+ case obj
+ when String
+ Utils.escape_html(obj)
+ else
+ Utils.escape_html(obj.inspect)
+ end
+ end
+
+ # :stopdoc:
+
+# adapted from Django
+# Copyright (c) 2005, the Lawrence Journal-World
+# Used under the modified BSD license:
+# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
+TEMPLATE = <<'HTML'
+
+
+
+
+ <%=h message %> at <%=h req.script_name + req.path_info %>
+
+
+
+
+
+
<%=h message %> (<%= status.to_i %>)
+
+
+
Request Method:
+
<%=h req.request_method %>
+
+
+
Request URL:
+
<%=h req.url %>
+
+
+
+
+
<%= detail %>
+
+
+
+
+ You're seeing this error because you use Rack::ShowStatus.
+
+
+
+
+HTML
+
+ # :startdoc:
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/static.rb b/vendor/gems/rack-1.1.0/lib/rack/static.rb
new file mode 100644
index 00000000..168e8f83
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/static.rb
@@ -0,0 +1,38 @@
+module Rack
+
+ # The Rack::Static middleware intercepts requests for static files
+ # (javascript files, images, stylesheets, etc) based on the url prefixes
+ # passed in the options, and serves them using a Rack::File object. This
+ # allows a Rack stack to serve both static and dynamic content.
+ #
+ # Examples:
+ # use Rack::Static, :urls => ["/media"]
+ # will serve all requests beginning with /media from the "media" folder
+ # located in the current directory (ie media/*).
+ #
+ # use Rack::Static, :urls => ["/css", "/images"], :root => "public"
+ # will serve all requests beginning with /css or /images from the folder
+ # "public" in the current directory (ie public/css/* and public/images/*)
+
+ class Static
+
+ def initialize(app, options={})
+ @app = app
+ @urls = options[:urls] || ["/favicon.ico"]
+ root = options[:root] || Dir.pwd
+ @file_server = Rack::File.new(root)
+ end
+
+ def call(env)
+ path = env["PATH_INFO"]
+ can_serve = @urls.any? { |url| path.index(url) == 0 }
+
+ if can_serve
+ @file_server.call(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/lib/rack/urlmap.rb b/vendor/gems/rack-1.1.0/lib/rack/urlmap.rb
new file mode 100644
index 00000000..b699d35b
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/urlmap.rb
@@ -0,0 +1,56 @@
+module Rack
+ # Rack::URLMap takes a hash mapping urls or paths to apps, and
+ # dispatches accordingly. Support for HTTP/1.1 host names exists if
+ # the URLs start with http:// or https://.
+ #
+ # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
+ # relevant for dispatch is in the SCRIPT_NAME, and the rest in the
+ # PATH_INFO. This should be taken care of when you need to
+ # reconstruct the URL in order to create links.
+ #
+ # URLMap dispatches in such a way that the longest paths are tried
+ # first, since they are most specific.
+
+ class URLMap
+ def initialize(map = {})
+ remap(map)
+ end
+
+ def remap(map)
+ @mapping = map.map { |location, app|
+ if location =~ %r{\Ahttps?://(.*?)(/.*)}
+ host, location = $1, $2
+ else
+ host = nil
+ end
+
+ unless location[0] == ?/
+ raise ArgumentError, "paths need to start with /"
+ end
+ location = location.chomp('/')
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
+
+ [host, location, match, app]
+ }.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
+ end
+
+ def call(env)
+ path = env["PATH_INFO"].to_s
+ script_name = env['SCRIPT_NAME']
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
+ @mapping.each { |host, location, match, app|
+ next unless (hHost == host || sName == host \
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
+ next unless path =~ match && rest = $1
+ next unless rest.empty? || rest[0] == ?/
+
+ return app.call(
+ env.merge(
+ 'SCRIPT_NAME' => (script_name + location),
+ 'PATH_INFO' => rest))
+ }
+ [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
+ end
+ end
+end
+
diff --git a/vendor/gems/rack-1.1.0/lib/rack/utils.rb b/vendor/gems/rack-1.1.0/lib/rack/utils.rb
new file mode 100644
index 00000000..68fd6ace
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/lib/rack/utils.rb
@@ -0,0 +1,620 @@
+# -*- encoding: binary -*-
+
+require 'set'
+require 'tempfile'
+
+module Rack
+ # Rack::Utils contains a grab-bag of useful methods for writing web
+ # applications adopted from all kinds of Ruby libraries.
+
+ module Utils
+ # Performs URI escaping so that you can construct proper
+ # query strings faster. Use this rather than the cgi.rb
+ # version since it's faster. (Stolen from Camping).
+ def escape(s)
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
+ }.tr(' ', '+')
+ end
+ module_function :escape
+
+ # Unescapes a URI escaped string. (Stolen from Camping).
+ def unescape(s)
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
+ [$1.delete('%')].pack('H*')
+ }
+ end
+ module_function :unescape
+
+ DEFAULT_SEP = /[&;] */n
+
+ # Stolen from Mongrel, with some small modifications:
+ # Parses a query string by breaking it up at the '&'
+ # and ';' characters. You can also use this to parse
+ # cookies by changing the characters used in the second
+ # parameter (which defaults to '&;').
+ def parse_query(qs, d = nil)
+ params = {}
+
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
+ k, v = p.split('=', 2).map { |x| unescape(x) }
+ if v =~ /^("|')(.*)\1$/
+ v = $2.gsub('\\'+$1, $1)
+ end
+ if cur = params[k]
+ if cur.class == Array
+ params[k] << v
+ else
+ params[k] = [cur, v]
+ end
+ else
+ params[k] = v
+ end
+ end
+
+ return params
+ end
+ module_function :parse_query
+
+ def parse_nested_query(qs, d = nil)
+ params = {}
+
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
+ k, v = unescape(p).split('=', 2)
+ normalize_params(params, k, v)
+ end
+
+ return params
+ end
+ module_function :parse_nested_query
+
+ def normalize_params(params, name, v = nil)
+ if v and v =~ /^("|')(.*)\1$/
+ v = $2.gsub('\\'+$1, $1)
+ end
+ name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
+ k = $1 || ''
+ after = $' || ''
+
+ return if k.empty?
+
+ if after == ""
+ params[k] = v
+ elsif after == "[]"
+ params[k] ||= []
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
+ params[k] << v
+ elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
+ child_key = $1
+ params[k] ||= []
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
+ if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
+ normalize_params(params[k].last, child_key, v)
+ else
+ params[k] << normalize_params({}, child_key, v)
+ end
+ else
+ params[k] ||= {}
+ raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
+ params[k] = normalize_params(params[k], after, v)
+ end
+
+ return params
+ end
+ module_function :normalize_params
+
+ def build_query(params)
+ params.map { |k, v|
+ if v.class == Array
+ build_query(v.map { |x| [k, x] })
+ else
+ "#{escape(k)}=#{escape(v)}"
+ end
+ }.join("&")
+ end
+ module_function :build_query
+
+ def build_nested_query(value, prefix = nil)
+ case value
+ when Array
+ value.map { |v|
+ build_nested_query(v, "#{prefix}[]")
+ }.join("&")
+ when Hash
+ value.map { |k, v|
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
+ }.join("&")
+ when String
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
+ "#{prefix}=#{escape(value)}"
+ else
+ prefix
+ end
+ end
+ module_function :build_nested_query
+
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
+ def escape_html(string)
+ string.to_s.gsub("&", "&").
+ gsub("<", "<").
+ gsub(">", ">").
+ gsub("'", "'").
+ gsub('"', """)
+ end
+ module_function :escape_html
+
+ def select_best_encoding(available_encodings, accept_encoding)
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+
+ expanded_accept_encoding =
+ accept_encoding.map { |m, q|
+ if m == "*"
+ (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
+ else
+ [[m, q]]
+ end
+ }.inject([]) { |mem, list|
+ mem + list
+ }
+
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
+
+ unless encoding_candidates.include?("identity")
+ encoding_candidates.push("identity")
+ end
+
+ expanded_accept_encoding.find_all { |m, q|
+ q == 0.0
+ }.each { |m, _|
+ encoding_candidates.delete(m)
+ }
+
+ return (encoding_candidates & available_encodings)[0]
+ end
+ module_function :select_best_encoding
+
+ def set_cookie_header!(header, key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; HttpOnly" if value[:httponly]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = escape(key) + "=" +
+ value.map { |v| escape v }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+
+ case header["Set-Cookie"]
+ when Array
+ header["Set-Cookie"] << cookie
+ when String
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie]
+ when nil
+ header["Set-Cookie"] = cookie
+ end
+
+ nil
+ end
+ module_function :set_cookie_header!
+
+ def delete_cookie_header!(header, key, value = {})
+ unless Array === header["Set-Cookie"]
+ header["Set-Cookie"] = [header["Set-Cookie"]].compact
+ end
+
+ header["Set-Cookie"].reject! { |cookie|
+ cookie =~ /\A#{escape(key)}=/
+ }
+
+ set_cookie_header!(header, key,
+ {:value => '', :path => nil, :domain => nil,
+ :expires => Time.at(0) }.merge(value))
+
+ nil
+ end
+ module_function :delete_cookie_header!
+
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
+ # String#bytesize under 1.9.
+ if ''.respond_to?(:bytesize)
+ def bytesize(string)
+ string.bytesize
+ end
+ else
+ def bytesize(string)
+ string.size
+ end
+ end
+ module_function :bytesize
+
+ # Context allows the use of a compatible middleware at different points
+ # in a request handling stack. A compatible middleware must define
+ # #context which should take the arguments env and app. The first of which
+ # would be the request environment. The second of which would be the rack
+ # application that the request would be forwarded to.
+ class Context
+ attr_reader :for, :app
+
+ def initialize(app_f, app_r)
+ raise 'running context does not respond to #context' unless app_f.respond_to? :context
+ @for, @app = app_f, app_r
+ end
+
+ def call(env)
+ @for.context(env, @app)
+ end
+
+ def recontext(app)
+ self.class.new(@for, app)
+ end
+
+ def context(env, app=@app)
+ recontext(app).call(env)
+ end
+ end
+
+ # A case-insensitive Hash that preserves the original case of a
+ # header when set.
+ class HeaderHash < Hash
+ def self.new(hash={})
+ HeaderHash === hash ? hash : super(hash)
+ end
+
+ def initialize(hash={})
+ super()
+ @names = {}
+ hash.each { |k, v| self[k] = v }
+ end
+
+ def each
+ super do |k, v|
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
+ end
+ end
+
+ def to_hash
+ inject({}) do |hash, (k,v)|
+ if v.respond_to? :to_ary
+ hash[k] = v.to_ary.join("\n")
+ else
+ hash[k] = v
+ end
+ hash
+ end
+ end
+
+ def [](k)
+ super(@names[k] ||= @names[k.downcase])
+ end
+
+ def []=(k, v)
+ delete k
+ @names[k] = @names[k.downcase] = k
+ super k, v
+ end
+
+ def delete(k)
+ canonical = k.downcase
+ result = super @names.delete(canonical)
+ @names.delete_if { |name,| name.downcase == canonical }
+ result
+ end
+
+ def include?(k)
+ @names.include?(k) || @names.include?(k.downcase)
+ end
+
+ alias_method :has_key?, :include?
+ alias_method :member?, :include?
+ alias_method :key?, :include?
+
+ def merge!(other)
+ other.each { |k, v| self[k] = v }
+ self
+ end
+
+ def merge(other)
+ hash = dup
+ hash.merge! other
+ end
+
+ def replace(other)
+ clear
+ other.each { |k, v| self[k] = v }
+ self
+ end
+ end
+
+ # Every standard HTTP code mapped to the appropriate message.
+ # Generated with:
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
+ HTTP_STATUS_CODES = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 226 => 'IM Used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Reserved',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 426 => 'Upgrade Required',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 510 => 'Not Extended',
+ }
+
+ # Responses with HTTP status codes that should not have an entity body
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
+
+ SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
+ hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
+ hash
+ }
+
+ def status_code(status)
+ if status.is_a?(Symbol)
+ SYMBOL_TO_STATUS_CODE[status] || 500
+ else
+ status.to_i
+ end
+ end
+ module_function :status_code
+
+ # A multipart form data parser, adapted from IOWA.
+ #
+ # Usually, Rack::Request#POST takes care of calling this.
+
+ module Multipart
+ class UploadedFile
+ # The filename, *not* including the path, of the "uploaded" file
+ attr_reader :original_filename
+
+ # The content type of the "uploaded" file
+ attr_accessor :content_type
+
+ def initialize(path, content_type = "text/plain", binary = false)
+ raise "#{path} file does not exist" unless ::File.exist?(path)
+ @content_type = content_type
+ @original_filename = ::File.basename(path)
+ @tempfile = Tempfile.new(@original_filename)
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
+ @tempfile.binmode if binary
+ FileUtils.copy_file(path, @tempfile.path)
+ end
+
+ def path
+ @tempfile.path
+ end
+ alias_method :local_path, :path
+
+ def method_missing(method_name, *args, &block) #:nodoc:
+ @tempfile.__send__(method_name, *args, &block)
+ end
+ end
+
+ EOL = "\r\n"
+ MULTIPART_BOUNDARY = "AaB03x"
+
+ def self.parse_multipart(env)
+ unless env['CONTENT_TYPE'] =~
+ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
+ nil
+ else
+ boundary = "--#{$1}"
+
+ params = {}
+ buf = ""
+ content_length = env['CONTENT_LENGTH'].to_i
+ input = env['rack.input']
+ input.rewind
+
+ boundary_size = Utils.bytesize(boundary) + EOL.size
+ bufsize = 16384
+
+ content_length -= boundary_size
+
+ read_buffer = ''
+
+ status = input.read(boundary_size, read_buffer)
+ raise EOFError, "bad content body" unless status == boundary + EOL
+
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
+
+ loop {
+ head = nil
+ body = ''
+ filename = content_type = name = nil
+
+ until head && buf =~ rx
+ if !head && i = buf.index(EOL+EOL)
+ head = buf.slice!(0, i+2) # First \r\n
+ buf.slice!(0, 2) # Second \r\n
+
+ filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
+ content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
+ name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
+
+ if content_type || filename
+ body = Tempfile.new("RackMultipart")
+ body.binmode if body.respond_to?(:binmode)
+ end
+
+ next
+ end
+
+ # Save the read body part.
+ if head && (boundary_size+4 < buf.size)
+ body << buf.slice!(0, buf.size - (boundary_size+4))
+ end
+
+ c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
+ raise EOFError, "bad content body" if c.nil? || c.empty?
+ buf << c
+ content_length -= c.size
+ end
+
+ # Save the rest.
+ if i = buf.index(rx)
+ body << buf.slice!(0, i)
+ buf.slice!(0, boundary_size+2)
+
+ content_length = -1 if $1 == "--"
+ end
+
+ if filename == ""
+ # filename is blank which means no file has been selected
+ data = nil
+ elsif filename
+ body.rewind
+
+ # Take the basename of the upload's original filename.
+ # This handles the full Windows paths given by Internet Explorer
+ # (and perhaps other broken user agents) without affecting
+ # those which give the lone filename.
+ filename =~ /^(?:.*[:\\\/])?(.*)/m
+ filename = $1
+
+ data = {:filename => filename, :type => content_type,
+ :name => name, :tempfile => body, :head => head}
+ elsif !filename && content_type
+ body.rewind
+
+ # Generic multipart cases, not coming from a form
+ data = {:type => content_type,
+ :name => name, :tempfile => body, :head => head}
+ else
+ data = body
+ end
+
+ Utils.normalize_params(params, name, data) unless data.nil?
+
+ # break if we're at the end of a buffer, but not if it is the end of a field
+ break if (buf.empty? && $1 != EOL) || content_length == -1
+ }
+
+ input.rewind
+
+ params
+ end
+ end
+
+ def self.build_multipart(params, first = true)
+ if first
+ unless params.is_a?(Hash)
+ raise ArgumentError, "value must be a Hash"
+ end
+
+ multipart = false
+ query = lambda { |value|
+ case value
+ when Array
+ value.each(&query)
+ when Hash
+ value.values.each(&query)
+ when UploadedFile
+ multipart = true
+ end
+ }
+ params.values.each(&query)
+ return nil unless multipart
+ end
+
+ flattened_params = Hash.new
+
+ params.each do |key, value|
+ k = first ? key.to_s : "[#{key}]"
+
+ case value
+ when Array
+ value.map { |v|
+ build_multipart(v, false).each { |subkey, subvalue|
+ flattened_params["#{k}[]#{subkey}"] = subvalue
+ }
+ }
+ when Hash
+ build_multipart(value, false).each { |subkey, subvalue|
+ flattened_params[k + subkey] = subvalue
+ }
+ else
+ flattened_params[k] = value
+ end
+ end
+
+ if first
+ flattened_params.map { |name, file|
+ if file.respond_to?(:original_filename)
+ ::File.open(file.path, "rb") do |f|
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
+<<-EOF
+--#{MULTIPART_BOUNDARY}\r
+Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
+Content-Type: #{file.content_type}\r
+Content-Length: #{::File.stat(file.path).size}\r
+\r
+#{f.read}\r
+EOF
+ end
+ else
+<<-EOF
+--#{MULTIPART_BOUNDARY}\r
+Content-Disposition: form-data; name="#{name}"\r
+\r
+#{file}\r
+EOF
+ end
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
+ else
+ flattened_params
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/rack.gemspec b/vendor/gems/rack-1.1.0/rack.gemspec
new file mode 100644
index 00000000..e28b9bb2
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/rack.gemspec
@@ -0,0 +1,38 @@
+Gem::Specification.new do |s|
+ s.name = "rack"
+ s.version = "1.1.0"
+ s.platform = Gem::Platform::RUBY
+ s.summary = "a modular Ruby webserver interface"
+
+ s.description = <<-EOF
+Rack provides minimal, modular and adaptable interface for developing
+web applications in Ruby. By wrapping HTTP requests and responses in
+the simplest way possible, it unifies and distills the API for web
+servers, web frameworks, and software in between (the so-called
+middleware) into a single method call.
+
+Also see http://rack.rubyforge.org.
+EOF
+
+ s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*}'] +
+ %w(COPYING KNOWN-ISSUES rack.gemspec RDOX README SPEC)
+ s.bindir = 'bin'
+ s.executables << 'rackup'
+ s.require_path = 'lib'
+ s.has_rdoc = true
+ s.extra_rdoc_files = ['README', 'SPEC', 'KNOWN-ISSUES']
+ s.test_files = Dir['test/{test,spec}_*.rb']
+
+ s.author = 'Christian Neukirchen'
+ s.email = 'chneukirchen@gmail.com'
+ s.homepage = 'http://rack.rubyforge.org'
+ s.rubyforge_project = 'rack'
+
+ s.add_development_dependency 'test-spec'
+
+ s.add_development_dependency 'camping'
+ s.add_development_dependency 'fcgi'
+ s.add_development_dependency 'memcache-client'
+ s.add_development_dependency 'mongrel'
+ s.add_development_dependency 'thin'
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_auth_basic.rb b/vendor/gems/rack-1.1.0/test/spec_rack_auth_basic.rb
new file mode 100644
index 00000000..0176efc8
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_auth_basic.rb
@@ -0,0 +1,73 @@
+require 'test/spec'
+
+require 'rack/auth/basic'
+require 'rack/mock'
+
+context 'Rack::Auth::Basic' do
+
+ def realm
+ 'WallysWorld'
+ end
+
+ def unprotected_app
+ lambda { |env| [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ] }
+ end
+
+ def protected_app
+ app = Rack::Auth::Basic.new(unprotected_app) { |username, password| 'Boss' == username }
+ app.realm = realm
+ app
+ end
+
+ setup do
+ @request = Rack::MockRequest.new(protected_app)
+ end
+
+ def request_with_basic_auth(username, password, &block)
+ request 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:#{password}"].pack("m*"), &block
+ end
+
+ def request(headers = {})
+ yield @request.get('/', headers)
+ end
+
+ def assert_basic_auth_challenge(response)
+ response.should.be.a.client_error
+ response.status.should.equal 401
+ response.should.include 'WWW-Authenticate'
+ response.headers['WWW-Authenticate'].should =~ /Basic realm="#{Regexp.escape(realm)}"/
+ response.body.should.be.empty
+ end
+
+ specify 'should challenge correctly when no credentials are specified' do
+ request do |response|
+ assert_basic_auth_challenge response
+ end
+ end
+
+ specify 'should rechallenge if incorrect credentials are specified' do
+ request_with_basic_auth 'joe', 'password' do |response|
+ assert_basic_auth_challenge response
+ end
+ end
+
+ specify 'should return application output if correct credentials are specified' do
+ request_with_basic_auth 'Boss', 'password' do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Boss'
+ end
+ end
+
+ specify 'should return 400 Bad Request if different auth scheme used' do
+ request 'HTTP_AUTHORIZATION' => 'Digest params' do |response|
+ response.should.be.a.client_error
+ response.status.should.equal 400
+ response.should.not.include 'WWW-Authenticate'
+ end
+ end
+
+ specify 'realm as optional constructor arg' do
+ app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
+ assert_equal realm, app.realm
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_auth_digest.rb b/vendor/gems/rack-1.1.0/test/spec_rack_auth_digest.rb
new file mode 100644
index 00000000..a980acc8
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_auth_digest.rb
@@ -0,0 +1,226 @@
+require 'test/spec'
+
+require 'rack/auth/digest/md5'
+require 'rack/mock'
+
+context 'Rack::Auth::Digest::MD5' do
+
+ def realm
+ 'WallysWorld'
+ end
+
+ def unprotected_app
+ lambda do |env|
+ [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ]
+ end
+ end
+
+ def protected_app
+ app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
+ { 'Alice' => 'correct-password' }[username]
+ end
+ app.realm = realm
+ app.opaque = 'this-should-be-secret'
+ app
+ end
+
+ def protected_app_with_hashed_passwords
+ app = Rack::Auth::Digest::MD5.new(unprotected_app) do |username|
+ username == 'Alice' ? Digest::MD5.hexdigest("Alice:#{realm}:correct-password") : nil
+ end
+ app.realm = realm
+ app.opaque = 'this-should-be-secret'
+ app.passwords_hashed = true
+ app
+ end
+
+ def partially_protected_app
+ Rack::URLMap.new({
+ '/' => unprotected_app,
+ '/protected' => protected_app
+ })
+ end
+
+ def protected_app_with_method_override
+ Rack::MethodOverride.new(protected_app)
+ end
+
+ setup do
+ @request = Rack::MockRequest.new(protected_app)
+ end
+
+ def request(method, path, headers = {}, &block)
+ response = @request.request(method, path, headers)
+ block.call(response) if block
+ return response
+ end
+
+ class MockDigestRequest
+ def initialize(params)
+ @params = params
+ end
+ def method_missing(sym)
+ if @params.has_key? k = sym.to_s
+ return @params[k]
+ end
+ super
+ end
+ def method
+ @params['method']
+ end
+ def response(password)
+ Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
+ end
+ end
+
+ def request_with_digest_auth(method, path, username, password, options = {}, &block)
+ request_options = {}
+ request_options[:input] = options.delete(:input) if options.include? :input
+
+ response = request(method, path, request_options)
+
+ return response unless response.status == 401
+
+ if wait = options.delete(:wait)
+ sleep wait
+ end
+
+ challenge = response['WWW-Authenticate'].split(' ', 2).last
+
+ params = Rack::Auth::Digest::Params.parse(challenge)
+
+ params['username'] = username
+ params['nc'] = '00000001'
+ params['cnonce'] = 'nonsensenonce'
+ params['uri'] = path
+
+ params['method'] = method
+
+ params.update options
+
+ params['response'] = MockDigestRequest.new(params).response(password)
+
+ request(method, path, request_options.merge('HTTP_AUTHORIZATION' => "Digest #{params}"), &block)
+ end
+
+ def assert_digest_auth_challenge(response)
+ response.should.be.a.client_error
+ response.status.should.equal 401
+ response.should.include 'WWW-Authenticate'
+ response.headers['WWW-Authenticate'].should =~ /^Digest /
+ response.body.should.be.empty
+ end
+
+ def assert_bad_request(response)
+ response.should.be.a.client_error
+ response.status.should.equal 400
+ response.should.not.include 'WWW-Authenticate'
+ end
+
+ specify 'should challenge when no credentials are specified' do
+ request 'GET', '/' do |response|
+ assert_digest_auth_challenge response
+ end
+ end
+
+ specify 'should return application output if correct credentials given' do
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password' do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Alice'
+ end
+ end
+
+ specify 'should return application output if correct credentials given (hashed passwords)' do
+ @request = Rack::MockRequest.new(protected_app_with_hashed_passwords)
+
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password' do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Alice'
+ end
+ end
+
+ specify 'should rechallenge if incorrect username given' do
+ request_with_digest_auth 'GET', '/', 'Bob', 'correct-password' do |response|
+ assert_digest_auth_challenge response
+ end
+ end
+
+ specify 'should rechallenge if incorrect password given' do
+ request_with_digest_auth 'GET', '/', 'Alice', 'wrong-password' do |response|
+ assert_digest_auth_challenge response
+ end
+ end
+
+ specify 'should rechallenge with stale parameter if nonce is stale' do
+ begin
+ Rack::Auth::Digest::Nonce.time_limit = 1
+
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 2 do |response|
+ assert_digest_auth_challenge response
+ response.headers['WWW-Authenticate'].should =~ /\bstale=true\b/
+ end
+ ensure
+ Rack::Auth::Digest::Nonce.time_limit = nil
+ end
+ end
+
+ specify 'should return 400 Bad Request if incorrect qop given' do
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', 'qop' => 'auth-int' do |response|
+ assert_bad_request response
+ end
+ end
+
+ specify 'should return 400 Bad Request if incorrect uri given' do
+ request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', 'uri' => '/foo' do |response|
+ assert_bad_request response
+ end
+ end
+
+ specify 'should return 400 Bad Request if different auth scheme used' do
+ request 'GET', '/', 'HTTP_AUTHORIZATION' => 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' do |response|
+ assert_bad_request response
+ end
+ end
+
+ specify 'should not require credentials for unprotected path' do
+ @request = Rack::MockRequest.new(partially_protected_app)
+ request 'GET', '/' do |response|
+ response.should.be.ok
+ end
+ end
+
+ specify 'should challenge when no credentials are specified for protected path' do
+ @request = Rack::MockRequest.new(partially_protected_app)
+ request 'GET', '/protected' do |response|
+ assert_digest_auth_challenge response
+ end
+ end
+
+ specify 'should return application output if correct credentials given for protected path' do
+ @request = Rack::MockRequest.new(partially_protected_app)
+ request_with_digest_auth 'GET', '/protected', 'Alice', 'correct-password' do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Alice'
+ end
+ end
+
+ specify 'should return application output if correct credentials given for POST' do
+ request_with_digest_auth 'POST', '/', 'Alice', 'correct-password' do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Alice'
+ end
+ end
+
+ specify 'should return application output if correct credentials given for PUT (using method override of POST)' do
+ @request = Rack::MockRequest.new(protected_app_with_method_override)
+ request_with_digest_auth 'POST', '/', 'Alice', 'correct-password', :input => "_method=put" do |response|
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Alice'
+ end
+ end
+
+ specify 'realm as optional constructor arg' do
+ app = Rack::Auth::Digest::MD5.new(unprotected_app, realm) { true }
+ assert_equal realm, app.realm
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_builder.rb b/vendor/gems/rack-1.1.0/test/spec_rack_builder.rb
new file mode 100644
index 00000000..3fad9810
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_builder.rb
@@ -0,0 +1,84 @@
+require 'test/spec'
+
+require 'rack/builder'
+require 'rack/mock'
+require 'rack/showexceptions'
+require 'rack/auth/basic'
+
+context "Rack::Builder" do
+ specify "chains apps by default" do
+ app = Rack::Builder.new do
+ use Rack::ShowExceptions
+ run lambda { |env| raise "bzzzt" }
+ end.to_app
+
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ end
+
+ specify "has implicit #to_app" do
+ app = Rack::Builder.new do
+ use Rack::ShowExceptions
+ run lambda { |env| raise "bzzzt" }
+ end
+
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ end
+
+ specify "supports blocks on use" do
+ app = Rack::Builder.new do
+ use Rack::ShowExceptions
+ use Rack::Auth::Basic do |username, password|
+ 'secret' == password
+ end
+
+ run lambda { |env| [200, {}, ['Hi Boss']] }
+ end
+
+ response = Rack::MockRequest.new(app).get("/")
+ response.should.be.client_error
+ response.status.should.equal 401
+
+ # with auth...
+ response = Rack::MockRequest.new(app).get("/",
+ 'HTTP_AUTHORIZATION' => 'Basic ' + ["joe:secret"].pack("m*"))
+ response.status.should.equal 200
+ response.body.to_s.should.equal 'Hi Boss'
+ end
+
+ specify "has explicit #to_app" do
+ app = Rack::Builder.app do
+ use Rack::ShowExceptions
+ run lambda { |env| raise "bzzzt" }
+ end
+
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ end
+
+ specify "apps are initialized once" do
+ app = Rack::Builder.new do
+ class AppClass
+ def initialize
+ @called = 0
+ end
+ def call(env)
+ raise "bzzzt" if @called > 0
+ @called += 1
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
+ end
+ end
+
+ use Rack::ShowExceptions
+ run AppClass.new
+ end
+
+ Rack::MockRequest.new(app).get("/").status.should.equal 200
+ Rack::MockRequest.new(app).get("/").should.be.server_error
+ end
+
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_camping.rb b/vendor/gems/rack-1.1.0/test/spec_rack_camping.rb
new file mode 100644
index 00000000..bed11710
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_camping.rb
@@ -0,0 +1,51 @@
+require 'test/spec'
+require 'stringio'
+require 'uri'
+
+begin
+ require 'rack/mock'
+
+ $-w, w = nil, $-w # yuck
+ require 'camping'
+ require 'rack/adapter/camping'
+
+ Camping.goes :CampApp
+ module CampApp
+ module Controllers
+ class HW < R('/')
+ def get
+ @headers["X-Served-By"] = URI("http://rack.rubyforge.org")
+ "Camping works!"
+ end
+
+ def post
+ "Data: #{input.foo}"
+ end
+ end
+ end
+ end
+ $-w = w
+
+ context "Rack::Adapter::Camping" do
+ specify "works with GET" do
+ res = Rack::MockRequest.new(Rack::Adapter::Camping.new(CampApp)).
+ get("/")
+
+ res.should.be.ok
+ res["Content-Type"].should.equal "text/html"
+ res["X-Served-By"].should.equal "http://rack.rubyforge.org"
+
+ res.body.should.equal "Camping works!"
+ end
+
+ specify "works with POST" do
+ res = Rack::MockRequest.new(Rack::Adapter::Camping.new(CampApp)).
+ post("/", :input => "foo=bar")
+
+ res.should.be.ok
+ res.body.should.equal "Data: bar"
+ end
+ end
+rescue LoadError
+ $stderr.puts "Skipping Rack::Adapter::Camping tests (Camping is required). `gem install camping` and try again."
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_cascade.rb b/vendor/gems/rack-1.1.0/test/spec_rack_cascade.rb
new file mode 100644
index 00000000..cf3c29b4
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_cascade.rb
@@ -0,0 +1,48 @@
+require 'test/spec'
+
+require 'rack/cascade'
+require 'rack/mock'
+
+require 'rack/urlmap'
+require 'rack/file'
+
+context "Rack::Cascade" do
+ docroot = File.expand_path(File.dirname(__FILE__))
+ app1 = Rack::File.new(docroot)
+
+ app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" })
+
+ app3 = Rack::URLMap.new("/foo" => lambda { |env|
+ [200, { "Content-Type" => "text/plain"}, [""]]})
+
+ specify "should dispatch onward on 404 by default" do
+ cascade = Rack::Cascade.new([app1, app2, app3])
+ Rack::MockRequest.new(cascade).get("/cgi/test").should.be.ok
+ Rack::MockRequest.new(cascade).get("/foo").should.be.ok
+ Rack::MockRequest.new(cascade).get("/toobad").should.be.not_found
+ Rack::MockRequest.new(cascade).get("/cgi/../bla").should.be.forbidden
+ end
+
+ specify "should dispatch onward on whatever is passed" do
+ cascade = Rack::Cascade.new([app1, app2, app3], [404, 403])
+ Rack::MockRequest.new(cascade).get("/cgi/../bla").should.be.not_found
+ end
+
+ specify "should return 404 if empty" do
+ Rack::MockRequest.new(Rack::Cascade.new([])).get('/').should.be.not_found
+ end
+
+ specify "should append new app" do
+ cascade = Rack::Cascade.new([], [404, 403])
+ Rack::MockRequest.new(cascade).get('/').should.be.not_found
+ cascade << app2
+ Rack::MockRequest.new(cascade).get('/cgi/test').should.be.not_found
+ Rack::MockRequest.new(cascade).get('/cgi/../bla').should.be.not_found
+ cascade << app1
+ Rack::MockRequest.new(cascade).get('/cgi/test').should.be.ok
+ Rack::MockRequest.new(cascade).get('/cgi/../bla').should.be.forbidden
+ Rack::MockRequest.new(cascade).get('/foo').should.be.not_found
+ cascade << app3
+ Rack::MockRequest.new(cascade).get('/foo').should.be.ok
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_cgi.rb b/vendor/gems/rack-1.1.0/test/spec_rack_cgi.rb
new file mode 100644
index 00000000..59500cd7
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_cgi.rb
@@ -0,0 +1,89 @@
+require 'test/spec'
+require 'testrequest'
+
+context "Rack::Handler::CGI" do
+ include TestRequest::Helpers
+
+ setup do
+ @host = '0.0.0.0'
+ @port = 9203
+ end
+
+ # Keep this first.
+ specify "startup" do
+ $pid = fork {
+ Dir.chdir(File.join(File.dirname(__FILE__), "..", "test", "cgi"))
+ exec "lighttpd -D -f lighttpd.conf"
+ }
+ end
+
+ specify "should respond" do
+ sleep 1
+ lambda {
+ GET("/test")
+ }.should.not.raise
+ end
+
+ specify "should be a lighttpd" do
+ GET("/test")
+ status.should.be 200
+ response["SERVER_SOFTWARE"].should =~ /lighttpd/
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
+ response["SERVER_PORT"].should.equal @port.to_s
+ response["SERVER_NAME"].should =~ @host
+ end
+
+ specify "should have rack headers" do
+ GET("/test")
+ response["rack.version"].should.equal [1,1]
+ response["rack.multithread"].should.be false
+ response["rack.multiprocess"].should.be true
+ response["rack.run_once"].should.be true
+ end
+
+ specify "should have CGI headers on GET" do
+ GET("/test")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal ""
+ response["QUERY_STRING"].should.equal ""
+ response["test.postdata"].should.equal ""
+
+ GET("/test/foo?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal "/foo"
+ response["QUERY_STRING"].should.equal "quux=1"
+ end
+
+ specify "should have CGI headers on POST" do
+ POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+ status.should.equal 200
+ response["REQUEST_METHOD"].should.equal "POST"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["QUERY_STRING"].should.equal ""
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
+ response["test.postdata"].should.equal "rack-form-data=23"
+ end
+
+ specify "should support HTTP auth" do
+ GET("/test", {:user => "ruth", :passwd => "secret"})
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
+ end
+
+ specify "should set status" do
+ GET("/test?secret")
+ status.should.equal 403
+ response["rack.url_scheme"].should.equal "http"
+ end
+
+ # Keep this last.
+ specify "shutdown" do
+ Process.kill 15, $pid
+ Process.wait($pid).should.equal $pid
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_chunked.rb b/vendor/gems/rack-1.1.0/test/spec_rack_chunked.rb
new file mode 100644
index 00000000..39eea482
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_chunked.rb
@@ -0,0 +1,62 @@
+require 'rack/mock'
+require 'rack/chunked'
+require 'rack/utils'
+
+context "Rack::Chunked" do
+
+ before do
+ @env = Rack::MockRequest.
+ env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET')
+ end
+
+ specify 'chunks responses with no Content-Length' do
+ app = lambda { |env| [200, {}, ['Hello', ' ', 'World!']] }
+ response = Rack::MockResponse.new(*Rack::Chunked.new(app).call(@env))
+ response.headers.should.not.include 'Content-Length'
+ response.headers['Transfer-Encoding'].should.equal 'chunked'
+ response.body.should.equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\n\r\n"
+ end
+
+ specify 'chunks empty bodies properly' do
+ app = lambda { |env| [200, {}, []] }
+ response = Rack::MockResponse.new(*Rack::Chunked.new(app).call(@env))
+ response.headers.should.not.include 'Content-Length'
+ response.headers['Transfer-Encoding'].should.equal 'chunked'
+ response.body.should.equal "0\r\n\r\n"
+ end
+
+ specify 'does not modify response when Content-Length header present' do
+ app = lambda { |env| [200, {'Content-Length'=>'12'}, ['Hello', ' ', 'World!']] }
+ status, headers, body = Rack::Chunked.new(app).call(@env)
+ status.should.equal 200
+ headers.should.not.include 'Transfer-Encoding'
+ headers.should.include 'Content-Length'
+ body.join.should.equal 'Hello World!'
+ end
+
+ specify 'does not modify response when client is HTTP/1.0' do
+ app = lambda { |env| [200, {}, ['Hello', ' ', 'World!']] }
+ @env['HTTP_VERSION'] = 'HTTP/1.0'
+ status, headers, body = Rack::Chunked.new(app).call(@env)
+ status.should.equal 200
+ headers.should.not.include 'Transfer-Encoding'
+ body.join.should.equal 'Hello World!'
+ end
+
+ specify 'does not modify response when Transfer-Encoding header already present' do
+ app = lambda { |env| [200, {'Transfer-Encoding' => 'identity'}, ['Hello', ' ', 'World!']] }
+ status, headers, body = Rack::Chunked.new(app).call(@env)
+ status.should.equal 200
+ headers['Transfer-Encoding'].should.equal 'identity'
+ body.join.should.equal 'Hello World!'
+ end
+
+ [100, 204, 304].each do |status_code|
+ specify "does not modify response when status code is #{status_code}" do
+ app = lambda { |env| [status_code, {}, []] }
+ status, headers, body = Rack::Chunked.new(app).call(@env)
+ status.should.equal status_code
+ headers.should.not.include 'Transfer-Encoding'
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_commonlogger.rb b/vendor/gems/rack-1.1.0/test/spec_rack_commonlogger.rb
new file mode 100644
index 00000000..46a72e86
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_commonlogger.rb
@@ -0,0 +1,61 @@
+require 'test/spec'
+require 'stringio'
+
+require 'rack/commonlogger'
+require 'rack/lobster'
+require 'rack/mock'
+
+context "Rack::CommonLogger" do
+ app = lambda { |env|
+ [200,
+ {"Content-Type" => "text/html", "Content-Length" => length.to_s},
+ [obj]]}
+ app_without_length = lambda { |env|
+ [200,
+ {"Content-Type" => "text/html"},
+ []]}
+ app_with_zero_length = lambda { |env|
+ [200,
+ {"Content-Type" => "text/html", "Content-Length" => "0"},
+ []]}
+
+ specify "should log to rack.errors by default" do
+ res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/")
+
+ res.errors.should.not.be.empty
+ res.errors.should =~ /"GET \/ " 200 #{length} /
+ end
+
+ specify "should log to anything with +write+" do
+ log = StringIO.new
+ res = Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
+
+ log.string.should =~ /"GET \/ " 200 #{length} /
+ end
+
+ specify "should log - content length if header is missing" do
+ res = Rack::MockRequest.new(Rack::CommonLogger.new(app_without_length)).get("/")
+
+ res.errors.should.not.be.empty
+ res.errors.should =~ /"GET \/ " 200 - /
+ end
+
+ specify "should log - content length if header is zero" do
+ res = Rack::MockRequest.new(Rack::CommonLogger.new(app_with_zero_length)).get("/")
+
+ res.errors.should.not.be.empty
+ res.errors.should =~ /"GET \/ " 200 - /
+ end
+
+ def length
+ self.class.length
+ end
+
+ def self.length
+ 123
+ end
+
+ def self.obj
+ "hello world"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_conditionalget.rb b/vendor/gems/rack-1.1.0/test/spec_rack_conditionalget.rb
new file mode 100644
index 00000000..ca34cc92
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_conditionalget.rb
@@ -0,0 +1,41 @@
+require 'test/spec'
+require 'time'
+
+require 'rack/mock'
+require 'rack/conditionalget'
+
+context "Rack::ConditionalGet" do
+ specify "should set a 304 status and truncate body when If-Modified-Since hits" do
+ timestamp = Time.now.httpdate
+ app = Rack::ConditionalGet.new(lambda { |env|
+ [200, {'Last-Modified'=>timestamp}, ['TEST']] })
+
+ response = Rack::MockRequest.new(app).
+ get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
+
+ response.status.should.equal 304
+ response.body.should.be.empty
+ end
+
+ specify "should set a 304 status and truncate body when If-None-Match hits" do
+ app = Rack::ConditionalGet.new(lambda { |env|
+ [200, {'Etag'=>'1234'}, ['TEST']] })
+
+ response = Rack::MockRequest.new(app).
+ get("/", 'HTTP_IF_NONE_MATCH' => '1234')
+
+ response.status.should.equal 304
+ response.body.should.be.empty
+ end
+
+ specify "should not affect non-GET/HEAD requests" do
+ app = Rack::ConditionalGet.new(lambda { |env|
+ [200, {'Etag'=>'1234'}, ['TEST']] })
+
+ response = Rack::MockRequest.new(app).
+ post("/", 'HTTP_IF_NONE_MATCH' => '1234')
+
+ response.status.should.equal 200
+ response.body.should.equal 'TEST'
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_config.rb b/vendor/gems/rack-1.1.0/test/spec_rack_config.rb
new file mode 100644
index 00000000..a508ea4b
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_config.rb
@@ -0,0 +1,24 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/builder'
+require 'rack/content_length'
+require 'rack/config'
+
+context "Rack::Config" do
+
+ specify "should accept a block that modifies the environment" do
+ app = Rack::Builder.new do
+ use Rack::Lint
+ use Rack::ContentLength
+ use Rack::Config do |env|
+ env['greeting'] = 'hello'
+ end
+ run lambda { |env|
+ [200, {'Content-Type' => 'text/plain'}, [env['greeting'] || '']]
+ }
+ end
+ response = Rack::MockRequest.new(app).get('/')
+ response.body.should.equal('hello')
+ end
+
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_content_length.rb b/vendor/gems/rack-1.1.0/test/spec_rack_content_length.rb
new file mode 100644
index 00000000..7db9345f
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_content_length.rb
@@ -0,0 +1,43 @@
+require 'rack/mock'
+require 'rack/content_length'
+
+context "Rack::ContentLength" do
+ specify "sets Content-Length on String bodies if none is set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.equal '13'
+ end
+
+ specify "sets Content-Length on Array bodies if none is set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.equal '13'
+ end
+
+ specify "does not set Content-Length on variable length bodies" do
+ body = lambda { "Hello World!" }
+ def body.each ; yield call ; end
+
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.be.nil
+ end
+
+ specify "does not change Content-Length if it is already set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '1'}, "Hello, World!"] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.equal '1'
+ end
+
+ specify "does not set Content-Length on 304 responses" do
+ app = lambda { |env| [304, {'Content-Type' => 'text/plain'}, []] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.equal nil
+ end
+
+ specify "does not set Content-Length when Transfer-Encoding is chunked" do
+ app = lambda { |env| [200, {'Transfer-Encoding' => 'chunked'}, []] }
+ response = Rack::ContentLength.new(app).call({})
+ response[1]['Content-Length'].should.equal nil
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_content_type.rb b/vendor/gems/rack-1.1.0/test/spec_rack_content_type.rb
new file mode 100644
index 00000000..9975b94d
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_content_type.rb
@@ -0,0 +1,30 @@
+require 'rack/mock'
+require 'rack/content_type'
+
+context "Rack::ContentType" do
+ specify "sets Content-Type to default text/html if none is set" do
+ app = lambda { |env| [200, {}, "Hello, World!"] }
+ status, headers, body = Rack::ContentType.new(app).call({})
+ headers['Content-Type'].should.equal 'text/html'
+ end
+
+ specify "sets Content-Type to chosen default if none is set" do
+ app = lambda { |env| [200, {}, "Hello, World!"] }
+ status, headers, body =
+ Rack::ContentType.new(app, 'application/octet-stream').call({})
+ headers['Content-Type'].should.equal 'application/octet-stream'
+ end
+
+ specify "does not change Content-Type if it is already set" do
+ app = lambda { |env| [200, {'Content-Type' => 'foo/bar'}, "Hello, World!"] }
+ status, headers, body = Rack::ContentType.new(app).call({})
+ headers['Content-Type'].should.equal 'foo/bar'
+ end
+
+ specify "case insensitive detection of Content-Type" do
+ app = lambda { |env| [200, {'CONTENT-Type' => 'foo/bar'}, "Hello, World!"] }
+ status, headers, body = Rack::ContentType.new(app).call({})
+ headers.to_a.select { |k,v| k.downcase == "content-type" }.
+ should.equal [["CONTENT-Type","foo/bar"]]
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_deflater.rb b/vendor/gems/rack-1.1.0/test/spec_rack_deflater.rb
new file mode 100644
index 00000000..c9bb3189
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_deflater.rb
@@ -0,0 +1,127 @@
+require 'test/spec'
+
+require 'rack/mock'
+require 'rack/deflater'
+require 'stringio'
+require 'time' # for Time#httpdate
+
+context "Rack::Deflater" do
+ def build_response(status, body, accept_encoding, headers = {})
+ body = [body] if body.respond_to? :to_str
+ app = lambda { |env| [status, {}, body] }
+ request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding))
+ response = Rack::Deflater.new(app).call(request)
+
+ return response
+ end
+
+ specify "should be able to deflate bodies that respond to each" do
+ body = Object.new
+ class << body; def each; yield("foo"); yield("bar"); end; end
+
+ response = build_response(200, body, "deflate")
+
+ response[0].should.equal(200)
+ response[1].should.equal({
+ "Content-Encoding" => "deflate",
+ "Vary" => "Accept-Encoding"
+ })
+ buf = ''
+ response[2].each { |part| buf << part }
+ buf.should.equal("K\313\317OJ,\002\000")
+ end
+
+ # TODO: This is really just a special case of the above...
+ specify "should be able to deflate String bodies" do
+ response = build_response(200, "Hello world!", "deflate")
+
+ response[0].should.equal(200)
+ response[1].should.equal({
+ "Content-Encoding" => "deflate",
+ "Vary" => "Accept-Encoding"
+ })
+ buf = ''
+ response[2].each { |part| buf << part }
+ buf.should.equal("\363H\315\311\311W(\317/\312IQ\004\000")
+ end
+
+ specify "should be able to gzip bodies that respond to each" do
+ body = Object.new
+ class << body; def each; yield("foo"); yield("bar"); end; end
+
+ response = build_response(200, body, "gzip")
+
+ response[0].should.equal(200)
+ response[1].should.equal({
+ "Content-Encoding" => "gzip",
+ "Vary" => "Accept-Encoding",
+ })
+
+ buf = ''
+ response[2].each { |part| buf << part }
+ io = StringIO.new(buf)
+ gz = Zlib::GzipReader.new(io)
+ gz.read.should.equal("foobar")
+ gz.close
+ end
+
+ specify "should be able to fallback to no deflation" do
+ response = build_response(200, "Hello world!", "superzip")
+
+ response[0].should.equal(200)
+ response[1].should.equal({ "Vary" => "Accept-Encoding" })
+ response[2].should.equal(["Hello world!"])
+ end
+
+ specify "should be able to skip when there is no response entity body" do
+ response = build_response(304, [], "gzip")
+
+ response[0].should.equal(304)
+ response[1].should.equal({})
+ response[2].should.equal([])
+ end
+
+ specify "should handle the lack of an acceptable encoding" do
+ response1 = build_response(200, "Hello world!", "identity;q=0", "PATH_INFO" => "/")
+ response1[0].should.equal(406)
+ response1[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "71"})
+ response1[2].should.equal(["An acceptable encoding for the requested resource / could not be found."])
+
+ response2 = build_response(200, "Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar")
+ response2[0].should.equal(406)
+ response2[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "78"})
+ response2[2].should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."])
+ end
+
+ specify "should handle gzip response with Last-Modified header" do
+ last_modified = Time.now.httpdate
+
+ app = lambda { |env| [200, { "Last-Modified" => last_modified }, ["Hello World!"]] }
+ request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
+ response = Rack::Deflater.new(app).call(request)
+
+ response[0].should.equal(200)
+ response[1].should.equal({
+ "Content-Encoding" => "gzip",
+ "Vary" => "Accept-Encoding",
+ "Last-Modified" => last_modified
+ })
+
+ buf = ''
+ response[2].each { |part| buf << part }
+ io = StringIO.new(buf)
+ gz = Zlib::GzipReader.new(io)
+ gz.read.should.equal("Hello World!")
+ gz.close
+ end
+
+ specify "should do nothing when no-transform Cache-Control directive present" do
+ app = lambda { |env| [200, {'Cache-Control' => 'no-transform'}, ['Hello World!']] }
+ request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
+ response = Rack::Deflater.new(app).call(request)
+
+ response[0].should.equal(200)
+ response[1].should.not.include "Content-Encoding"
+ response[2].join.should.equal("Hello World!")
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_directory.rb b/vendor/gems/rack-1.1.0/test/spec_rack_directory.rb
new file mode 100644
index 00000000..d255c91d
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_directory.rb
@@ -0,0 +1,61 @@
+require 'test/spec'
+
+require 'rack/directory'
+require 'rack/lint'
+
+require 'rack/mock'
+
+context "Rack::Directory" do
+ DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
+ FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, ['passed!']] }
+ app = Rack::Directory.new DOCROOT, FILE_CATCH
+
+ specify "serves directory indices" do
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/")
+
+ res.should.be.ok
+ res.should =~ //
+ end
+
+ specify "passes to app if file found" do
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/test")
+
+ res.should.be.ok
+ res.should =~ /passed!/
+ end
+
+ specify "serves uri with URL encoded filenames" do
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/%63%67%69/") # "/cgi/test"
+
+ res.should.be.ok
+ res.should =~ //
+
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/%74%65%73%74") # "/cgi/test"
+
+ res.should.be.ok
+ res.should =~ /passed!/
+ end
+
+ specify "does not allow directory traversal" do
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/../test")
+
+ res.should.be.forbidden
+
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/%2E%2E/test")
+
+ res.should.be.forbidden
+ end
+
+ specify "404s if it can't find the file" do
+ res = Rack::MockRequest.new(Rack::Lint.new(app)).
+ get("/cgi/blubb")
+
+ res.should.be.not_found
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_etag.rb b/vendor/gems/rack-1.1.0/test/spec_rack_etag.rb
new file mode 100644
index 00000000..73cd31ac
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_etag.rb
@@ -0,0 +1,17 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/etag'
+
+context "Rack::ETag" do
+ specify "sets ETag if none is set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+ response = Rack::ETag.new(app).call({})
+ response[1]['ETag'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
+ end
+
+ specify "does not change ETag if it is already set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, ["Hello, World!"]] }
+ response = Rack::ETag.new(app).call({})
+ response[1]['ETag'].should.equal "\"abc\""
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_fastcgi.rb b/vendor/gems/rack-1.1.0/test/spec_rack_fastcgi.rb
new file mode 100644
index 00000000..1ae55ace
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_fastcgi.rb
@@ -0,0 +1,89 @@
+require 'test/spec'
+require 'testrequest'
+
+context "Rack::Handler::FastCGI" do
+ include TestRequest::Helpers
+
+ setup do
+ @host = '0.0.0.0'
+ @port = 9203
+ end
+
+ # Keep this first.
+ specify "startup" do
+ $pid = fork {
+ Dir.chdir(File.join(File.dirname(__FILE__), "..", "test", "cgi"))
+ exec "lighttpd -D -f lighttpd.conf"
+ }
+ end
+
+ specify "should respond" do
+ sleep 1
+ lambda {
+ GET("/test.fcgi")
+ }.should.not.raise
+ end
+
+ specify "should be a lighttpd" do
+ GET("/test.fcgi")
+ status.should.be 200
+ response["SERVER_SOFTWARE"].should =~ /lighttpd/
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
+ response["SERVER_PORT"].should.equal @port.to_s
+ response["SERVER_NAME"].should =~ @host
+ end
+
+ specify "should have rack headers" do
+ GET("/test.fcgi")
+ response["rack.version"].should.equal [1,1]
+ response["rack.multithread"].should.be false
+ response["rack.multiprocess"].should.be true
+ response["rack.run_once"].should.be false
+ end
+
+ specify "should have CGI headers on GET" do
+ GET("/test.fcgi")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal ""
+ response["QUERY_STRING"].should.equal ""
+ response["test.postdata"].should.equal ""
+
+ GET("/test.fcgi/foo?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal "/foo"
+ response["QUERY_STRING"].should.equal "quux=1"
+ end
+
+ specify "should have CGI headers on POST" do
+ POST("/test.fcgi", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+ status.should.equal 200
+ response["REQUEST_METHOD"].should.equal "POST"
+ response["SCRIPT_NAME"].should.equal "/test.fcgi"
+ response["REQUEST_PATH"].should.equal "/"
+ response["QUERY_STRING"].should.equal ""
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
+ response["test.postdata"].should.equal "rack-form-data=23"
+ end
+
+ specify "should support HTTP auth" do
+ GET("/test.fcgi", {:user => "ruth", :passwd => "secret"})
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
+ end
+
+ specify "should set status" do
+ GET("/test.fcgi?secret")
+ status.should.equal 403
+ response["rack.url_scheme"].should.equal "http"
+ end
+
+ # Keep this last.
+ specify "shutdown" do
+ Process.kill 15, $pid
+ Process.wait($pid).should.equal $pid
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_file.rb b/vendor/gems/rack-1.1.0/test/spec_rack_file.rb
new file mode 100644
index 00000000..0a2f8ee8
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_file.rb
@@ -0,0 +1,75 @@
+require 'test/spec'
+
+require 'rack/file'
+require 'rack/lint'
+
+require 'rack/mock'
+
+context "Rack::File" do
+ DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
+
+ specify "serves files" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi/test")
+
+ res.should.be.ok
+ res.should =~ /ruby/
+ end
+
+ specify "sets Last-Modified header" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi/test")
+
+ path = File.join(DOCROOT, "/cgi/test")
+
+ res.should.be.ok
+ res["Last-Modified"].should.equal File.mtime(path).httpdate
+ end
+
+ specify "serves files with URL encoded filenames" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi/%74%65%73%74") # "/cgi/test"
+
+ res.should.be.ok
+ res.should =~ /ruby/
+ end
+
+ specify "does not allow directory traversal" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi/../test")
+
+ res.should.be.forbidden
+ end
+
+ specify "does not allow directory traversal with encoded periods" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/%2E%2E/README")
+
+ res.should.be.forbidden
+ end
+
+ specify "404s if it can't find the file" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi/blubb")
+
+ res.should.be.not_found
+ end
+
+ specify "detects SystemCallErrors" do
+ res = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))).
+ get("/cgi")
+
+ res.should.be.not_found
+ end
+
+ specify "returns bodies that respond to #to_path" do
+ env = Rack::MockRequest.env_for("/cgi/test")
+ status, headers, body = Rack::File.new(DOCROOT).call(env)
+
+ path = File.join(DOCROOT, "/cgi/test")
+
+ status.should.equal 200
+ body.should.respond_to :to_path
+ body.to_path.should.equal path
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_handler.rb b/vendor/gems/rack-1.1.0/test/spec_rack_handler.rb
new file mode 100644
index 00000000..fcf19b78
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_handler.rb
@@ -0,0 +1,43 @@
+require 'test/spec'
+
+require 'rack/handler'
+
+class Rack::Handler::Lobster; end
+class RockLobster; end
+
+context "Rack::Handler" do
+ specify "has registered default handlers" do
+ Rack::Handler.get('cgi').should.equal Rack::Handler::CGI
+ Rack::Handler.get('fastcgi').should.equal Rack::Handler::FastCGI
+ Rack::Handler.get('mongrel').should.equal Rack::Handler::Mongrel
+ Rack::Handler.get('webrick').should.equal Rack::Handler::WEBrick
+ end
+
+ specify "handler that doesn't exist should raise a NameError" do
+ lambda {
+ Rack::Handler.get('boom')
+ }.should.raise(NameError)
+ end
+
+ specify "should get unregistered, but already required, handler by name" do
+ Rack::Handler.get('Lobster').should.equal Rack::Handler::Lobster
+ end
+
+ specify "should register custom handler" do
+ Rack::Handler.register('rock_lobster', 'RockLobster')
+ Rack::Handler.get('rock_lobster').should.equal RockLobster
+ end
+
+ specify "should not need registration for properly coded handlers even if not already required" do
+ begin
+ $:.push "test/unregistered_handler"
+ Rack::Handler.get('Unregistered').should.equal Rack::Handler::Unregistered
+ lambda {
+ Rack::Handler.get('UnRegistered')
+ }.should.raise(NameError)
+ Rack::Handler.get('UnregisteredLongOne').should.equal Rack::Handler::UnregisteredLongOne
+ ensure
+ $:.delete "test/unregistered_handler"
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_head.rb b/vendor/gems/rack-1.1.0/test/spec_rack_head.rb
new file mode 100644
index 00000000..48d3f81f
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_head.rb
@@ -0,0 +1,30 @@
+require 'rack/head'
+require 'rack/mock'
+
+context "Rack::Head" do
+ def test_response(headers = {})
+ app = lambda { |env| [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]] }
+ request = Rack::MockRequest.env_for("/", headers)
+ response = Rack::Head.new(app).call(request)
+
+ return response
+ end
+
+ specify "passes GET, POST, PUT, DELETE, OPTIONS, TRACE requests" do
+ %w[GET POST PUT DELETE OPTIONS TRACE].each do |type|
+ resp = test_response("REQUEST_METHOD" => type)
+
+ resp[0].should.equal(200)
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
+ resp[2].should.equal(["foo"])
+ end
+ end
+
+ specify "removes body from HEAD requests" do
+ resp = test_response("REQUEST_METHOD" => "HEAD")
+
+ resp[0].should.equal(200)
+ resp[1].should.equal({"Content-type" => "test/plain", "Content-length" => "3"})
+ resp[2].should.equal([])
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_lint.rb b/vendor/gems/rack-1.1.0/test/spec_rack_lint.rb
new file mode 100644
index 00000000..bbf75c17
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_lint.rb
@@ -0,0 +1,528 @@
+require 'test/spec'
+require 'stringio'
+
+require 'rack/lint'
+require 'rack/mock'
+
+context "Rack::Lint" do
+ def env(*args)
+ Rack::MockRequest.env_for("/", *args)
+ end
+
+ specify "passes valid request" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
+ }).call(env({}))
+ }.should.not.raise
+ end
+
+ specify "notices fatal errors" do
+ lambda { Rack::Lint.new(nil).call }.should.raise(Rack::Lint::LintError).
+ message.should.match(/No env given/)
+ end
+
+ specify "notices environment errors" do
+ lambda { Rack::Lint.new(nil).call 5 }.should.raise(Rack::Lint::LintError).
+ message.should.match(/not a Hash/)
+
+ lambda {
+ e = env
+ e.delete("REQUEST_METHOD")
+ Rack::Lint.new(nil).call(e)
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/missing required key REQUEST_METHOD/)
+
+ lambda {
+ e = env
+ e.delete("SERVER_NAME")
+ Rack::Lint.new(nil).call(e)
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/missing required key SERVER_NAME/)
+
+
+ lambda {
+ Rack::Lint.new(nil).call(env("HTTP_CONTENT_TYPE" => "text/plain"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/contains HTTP_CONTENT_TYPE/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("HTTP_CONTENT_LENGTH" => "42"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/contains HTTP_CONTENT_LENGTH/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("FOO" => Object.new))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/non-string value/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.version" => "0.2"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must be an Array/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.url_scheme" => "gopher"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/url_scheme unknown/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.session" => []))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("session [] must respond to store and []=")
+
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.logger" => []))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("logger [] must respond to info")
+
+ lambda {
+ Rack::Lint.new(nil).call(env("REQUEST_METHOD" => "FUCKUP?"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/REQUEST_METHOD/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "howdy"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must start with/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("PATH_INFO" => "../foo"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must start with/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("CONTENT_LENGTH" => "xcii"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Invalid CONTENT_LENGTH/)
+
+ lambda {
+ e = env
+ e.delete("PATH_INFO")
+ e.delete("SCRIPT_NAME")
+ Rack::Lint.new(nil).call(e)
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/One of .* must be set/)
+
+ lambda {
+ Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "/"))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/cannot be .* make it ''/)
+ end
+
+ specify "notices input errors" do
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.input" => ""))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/does not respond to #gets/)
+
+ lambda {
+ input = Object.new
+ def input.binmode?
+ false
+ end
+ Rack::Lint.new(nil).call(env("rack.input" => input))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/is not opened in binary mode/)
+
+ lambda {
+ input = Object.new
+ def input.external_encoding
+ result = Object.new
+ def result.name
+ "US-ASCII"
+ end
+ result
+ end
+ Rack::Lint.new(nil).call(env("rack.input" => input))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/does not have ASCII-8BIT as its external encoding/)
+ end
+
+ specify "notices error errors" do
+ lambda {
+ Rack::Lint.new(nil).call(env("rack.errors" => ""))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/does not respond to #puts/)
+ end
+
+ specify "notices status errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ ["cc", {}, ""]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must be >=100 seen as integer/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [42, {}, ""]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must be >=100 seen as integer/)
+ end
+
+ specify "notices header errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, Object.new, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("headers object should respond to #each, but doesn't (got Object as headers)")
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {true=>false}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("header key must be a string, was TrueClass")
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Status" => "404"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must not contain Status/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-Type:" => "text/plain"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must not contain :/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-" => "text/plain"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/must not end/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"..%%quark%%.." => "text/plain"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("invalid header name: ..%%quark%%..")
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Foo" => Object.new}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Object")
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Foo" => [1, 2, 3]}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.equal("a header value must be a String, but the value of 'Foo' is a Array")
+
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Foo-Bar" => "text\000plain"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/invalid header/)
+
+ # line ends (010) should be allowed in header values.
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []]
+ }).call(env({}))
+ }.should.not.raise(Rack::Lint::LintError)
+ end
+
+ specify "notices content-type errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/No Content-Type/)
+
+ [100, 101, 204, 304].each do |status|
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Content-Type header found/)
+ end
+ end
+
+ specify "notices content-length errors" do
+ [100, 101, 204, 304].each do |status|
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [status, {"Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Content-Length header found/)
+ end
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Content-Length header was 1, but should be 0/)
+ end
+
+ specify "notices body errors" do
+ lambda {
+ status, header, body = Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]]
+ }).call(env({}))
+ body.each { |part| }
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/yielded non-string/)
+ end
+
+ specify "notices input handling errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].gets("\r\n")
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/gets called with arguments/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(1, 2, 3)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read called with too many arguments/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read("foo")
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read called with non-integer and non-nil length/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(-1)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read called with a negative length/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(nil, nil)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read called with non-String buffer/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(nil, 1)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read called with non-String buffer/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].rewind(0)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/rewind called with arguments/)
+
+ weirdio = Object.new
+ class << weirdio
+ def gets
+ 42
+ end
+
+ def read
+ 23
+ end
+
+ def each
+ yield 23
+ yield 42
+ end
+
+ def rewind
+ raise Errno::ESPIPE, "Errno::ESPIPE"
+ end
+ end
+
+ eof_weirdio = Object.new
+ class << eof_weirdio
+ def gets
+ nil
+ end
+
+ def read(*args)
+ nil
+ end
+
+ def each
+ end
+
+ def rewind
+ end
+ end
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].gets
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env("rack.input" => weirdio))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/gets didn't return a String/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].each { |x| }
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env("rack.input" => weirdio))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/each didn't yield a String/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env("rack.input" => weirdio))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read didn't return nil or a String/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env("rack.input" => eof_weirdio))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/read\(nil\) returned nil on EOF/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].rewind
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env("rack.input" => weirdio))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/rewind raised Errno::ESPIPE/)
+
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].close
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/close must not be called/)
+ end
+
+ specify "notices error handling errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.errors"].write(42)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/write not called with a String/)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.errors"].close
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/close must not be called/)
+ end
+
+ specify "notices HEAD errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, []]
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))
+ }.should.not.raise
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
+ }).call(env({"REQUEST_METHOD" => "HEAD"}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/body was given for HEAD/)
+ end
+
+ specify "passes valid read calls" do
+ hello_str = "hello world"
+ hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(0)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(1)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(nil)
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(nil, '')
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ env["rack.input"].read(1, '')
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
+ }.should.not.raise(Rack::Lint::LintError)
+ end
+end
+
+context "Rack::Lint::InputWrapper" do
+ specify "delegates :size to underlying IO object" do
+ class IOMock
+ def size
+ 101
+ end
+ end
+
+ wrapper = Rack::Lint::InputWrapper.new(IOMock.new)
+ wrapper.size.should == 101
+ end
+
+ specify "delegates :rewind to underlying IO object" do
+ io = StringIO.new("123")
+ wrapper = Rack::Lint::InputWrapper.new(io)
+ wrapper.read.should.equal "123"
+ wrapper.read.should.equal ""
+ wrapper.rewind
+ wrapper.read.should.equal "123"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_lobster.rb b/vendor/gems/rack-1.1.0/test/spec_rack_lobster.rb
new file mode 100644
index 00000000..7be267a2
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_lobster.rb
@@ -0,0 +1,45 @@
+require 'test/spec'
+
+require 'rack/lobster'
+require 'rack/mock'
+
+context "Rack::Lobster::LambdaLobster" do
+ specify "should be a single lambda" do
+ Rack::Lobster::LambdaLobster.should.be.kind_of Proc
+ end
+
+ specify "should look like a lobster" do
+ res = Rack::MockRequest.new(Rack::Lobster::LambdaLobster).get("/")
+ res.should.be.ok
+ res.body.should.include "(,(,,(,,,("
+ res.body.should.include "?flip"
+ end
+
+ specify "should be flippable" do
+ res = Rack::MockRequest.new(Rack::Lobster::LambdaLobster).get("/?flip")
+ res.should.be.ok
+ res.body.should.include "(,,,(,,(,("
+ end
+end
+
+context "Rack::Lobster" do
+ specify "should look like a lobster" do
+ res = Rack::MockRequest.new(Rack::Lobster.new).get("/")
+ res.should.be.ok
+ res.body.should.include "(,(,,(,,,("
+ res.body.should.include "?flip"
+ res.body.should.include "crash"
+ end
+
+ specify "should be flippable" do
+ res = Rack::MockRequest.new(Rack::Lobster.new).get("/?flip=left")
+ res.should.be.ok
+ res.body.should.include "(,,,(,,(,("
+ end
+
+ specify "should provide crashing for testing purposes" do
+ lambda {
+ Rack::MockRequest.new(Rack::Lobster.new).get("/?flip=crash")
+ }.should.raise
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_lock.rb b/vendor/gems/rack-1.1.0/test/spec_rack_lock.rb
new file mode 100644
index 00000000..18af2b23
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_lock.rb
@@ -0,0 +1,38 @@
+require 'test/spec'
+
+require 'rack/mock'
+require 'rack/lock'
+
+context "Rack::Lock" do
+ class Lock
+ attr_reader :synchronized
+
+ def initialize
+ @synchronized = false
+ end
+
+ def synchronize
+ @synchronized = true
+ yield
+ end
+ end
+
+ specify "should call synchronize on lock" do
+ lock = Lock.new
+ env = Rack::MockRequest.env_for("/")
+ app = Rack::Lock.new(lambda { |env| }, lock)
+ lock.synchronized.should.equal false
+ app.call(env)
+ lock.synchronized.should.equal true
+ end
+
+ specify "should set multithread flag to false" do
+ app = Rack::Lock.new(lambda { |env| env['rack.multithread'] })
+ app.call(Rack::MockRequest.env_for("/")).should.equal false
+ end
+
+ specify "should reset original multithread flag when exiting lock" do
+ app = Rack::Lock.new(lambda { |env| env })
+ app.call(Rack::MockRequest.env_for("/"))['rack.multithread'].should.equal true
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_logger.rb b/vendor/gems/rack-1.1.0/test/spec_rack_logger.rb
new file mode 100644
index 00000000..d55b9c77
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_logger.rb
@@ -0,0 +1,21 @@
+require 'rack/logger'
+require 'rack/lint'
+require 'stringio'
+
+context "Rack::Logger" do
+ specify "logs to rack.errors" do
+ app = lambda { |env|
+ log = env['rack.logger']
+ log.debug("Created logger")
+ log.info("Program started")
+ log.warn("Nothing to do!")
+
+ [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]]
+ }
+
+ errors = StringIO.new
+ Rack::Logger.new(app).call({'rack.errors' => errors})
+ errors.string.should.match "INFO -- : Program started"
+ errors.string.should.match "WARN -- : Nothing to do"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_methodoverride.rb b/vendor/gems/rack-1.1.0/test/spec_rack_methodoverride.rb
new file mode 100644
index 00000000..57452394
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_methodoverride.rb
@@ -0,0 +1,60 @@
+require 'test/spec'
+
+require 'rack/mock'
+require 'rack/methodoverride'
+require 'stringio'
+
+context "Rack::MethodOverride" do
+ specify "should not affect GET requests" do
+ env = Rack::MockRequest.env_for("/?_method=delete", :method => "GET")
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["REQUEST_METHOD"].should.equal "GET"
+ end
+
+ specify "_method parameter should modify REQUEST_METHOD for POST requests" do
+ env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=put")
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["REQUEST_METHOD"].should.equal "PUT"
+ end
+
+ specify "X-HTTP-Method-Override header should modify REQUEST_METHOD for POST requests" do
+ env = Rack::MockRequest.env_for("/",
+ :method => "POST",
+ "HTTP_X_HTTP_METHOD_OVERRIDE" => "PUT"
+ )
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["REQUEST_METHOD"].should.equal "PUT"
+ end
+
+ specify "should not modify REQUEST_METHOD if the method is unknown" do
+ env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=foo")
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["REQUEST_METHOD"].should.equal "POST"
+ end
+
+ specify "should not modify REQUEST_METHOD when _method is nil" do
+ env = Rack::MockRequest.env_for("/", :method => "POST", :input => "foo=bar")
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["REQUEST_METHOD"].should.equal "POST"
+ end
+
+ specify "should store the original REQUEST_METHOD prior to overriding" do
+ env = Rack::MockRequest.env_for("/",
+ :method => "POST",
+ :input => "_method=options")
+ app = Rack::MethodOverride.new(lambda { |env| Rack::Request.new(env) })
+ req = app.call(env)
+
+ req.env["rack.methodoverride.original_method"].should.equal "POST"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_mock.rb b/vendor/gems/rack-1.1.0/test/spec_rack_mock.rb
new file mode 100644
index 00000000..a03bedc2
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_mock.rb
@@ -0,0 +1,243 @@
+require 'yaml'
+require 'rack/mock'
+require 'rack/request'
+require 'rack/response'
+
+app = lambda { |env|
+ req = Rack::Request.new(env)
+
+ env["mock.postdata"] = env["rack.input"].read
+ if req.GET["error"]
+ env["rack.errors"].puts req.GET["error"]
+ env["rack.errors"].flush
+ end
+
+ Rack::Response.new(env.to_yaml,
+ req.GET["status"] || 200,
+ "Content-Type" => "text/yaml").finish
+}
+
+context "Rack::MockRequest" do
+ specify "should return a MockResponse" do
+ res = Rack::MockRequest.new(app).get("")
+ res.should.be.kind_of Rack::MockResponse
+ end
+
+ specify "should be able to only return the environment" do
+ env = Rack::MockRequest.env_for("")
+ env.should.be.kind_of Hash
+ env.should.include "rack.version"
+ end
+
+ specify "should provide sensible defaults" do
+ res = Rack::MockRequest.new(app).request
+
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["SERVER_NAME"].should.equal "example.org"
+ env["SERVER_PORT"].should.equal "80"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/"
+ env["SCRIPT_NAME"].should.equal ""
+ env["rack.url_scheme"].should.equal "http"
+ env["mock.postdata"].should.be.empty
+ end
+
+ specify "should allow GET/POST/PUT/DELETE" do
+ res = Rack::MockRequest.new(app).get("", :input => "foo")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+
+ res = Rack::MockRequest.new(app).post("", :input => "foo")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "POST"
+
+ res = Rack::MockRequest.new(app).put("", :input => "foo")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "PUT"
+
+ res = Rack::MockRequest.new(app).delete("", :input => "foo")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "DELETE"
+
+ Rack::MockRequest.env_for("/", :method => "OPTIONS")["REQUEST_METHOD"].
+ should.equal "OPTIONS"
+ end
+
+ specify "should set content length" do
+ env = Rack::MockRequest.env_for("/", :input => "foo")
+ env["CONTENT_LENGTH"].should.equal "3"
+ end
+
+ specify "should allow posting" do
+ res = Rack::MockRequest.new(app).get("", :input => "foo")
+ env = YAML.load(res.body)
+ env["mock.postdata"].should.equal "foo"
+
+ res = Rack::MockRequest.new(app).post("", :input => StringIO.new("foo"))
+ env = YAML.load(res.body)
+ env["mock.postdata"].should.equal "foo"
+ end
+
+ specify "should use all parts of an URL" do
+ res = Rack::MockRequest.new(app).
+ get("https://bla.example.org:9292/meh/foo?bar")
+ res.should.be.kind_of Rack::MockResponse
+
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["SERVER_NAME"].should.equal "bla.example.org"
+ env["SERVER_PORT"].should.equal "9292"
+ env["QUERY_STRING"].should.equal "bar"
+ env["PATH_INFO"].should.equal "/meh/foo"
+ env["rack.url_scheme"].should.equal "https"
+ end
+
+ specify "should set SSL port and HTTP flag on when using https" do
+ res = Rack::MockRequest.new(app).
+ get("https://example.org/foo")
+ res.should.be.kind_of Rack::MockResponse
+
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["SERVER_NAME"].should.equal "example.org"
+ env["SERVER_PORT"].should.equal "443"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/foo"
+ env["rack.url_scheme"].should.equal "https"
+ env["HTTPS"].should.equal "on"
+ end
+
+ specify "should prepend slash to uri path" do
+ res = Rack::MockRequest.new(app).
+ get("foo")
+ res.should.be.kind_of Rack::MockResponse
+
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["SERVER_NAME"].should.equal "example.org"
+ env["SERVER_PORT"].should.equal "80"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/foo"
+ env["rack.url_scheme"].should.equal "http"
+ end
+
+ specify "should properly convert method name to an uppercase string" do
+ res = Rack::MockRequest.new(app).request(:get)
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ end
+
+ specify "should accept params and build query string for GET requests" do
+ res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => {:foo => {:bar => "1"}})
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["QUERY_STRING"].should.match "baz=2"
+ env["QUERY_STRING"].should.match "foo[bar]=1"
+ env["PATH_INFO"].should.equal "/foo"
+ env["mock.postdata"].should.equal ""
+ end
+
+ specify "should accept raw input in params for GET requests" do
+ res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => "foo[bar]=1")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "GET"
+ env["QUERY_STRING"].should.match "baz=2"
+ env["QUERY_STRING"].should.match "foo[bar]=1"
+ env["PATH_INFO"].should.equal "/foo"
+ env["mock.postdata"].should.equal ""
+ end
+
+ specify "should accept params and build url encoded params for POST requests" do
+ res = Rack::MockRequest.new(app).post("/foo", :params => {:foo => {:bar => "1"}})
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "POST"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/foo"
+ env["CONTENT_TYPE"].should.equal "application/x-www-form-urlencoded"
+ env["mock.postdata"].should.equal "foo[bar]=1"
+ end
+
+ specify "should accept raw input in params for POST requests" do
+ res = Rack::MockRequest.new(app).post("/foo", :params => "foo[bar]=1")
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "POST"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/foo"
+ env["CONTENT_TYPE"].should.equal "application/x-www-form-urlencoded"
+ env["mock.postdata"].should.equal "foo[bar]=1"
+ end
+
+ specify "should accept params and build multipart encoded params for POST requests" do
+ files = Rack::Utils::Multipart::UploadedFile.new(File.join(File.dirname(__FILE__), "multipart", "file1.txt"))
+ res = Rack::MockRequest.new(app).post("/foo", :params => { "submit-name" => "Larry", "files" => files })
+ env = YAML.load(res.body)
+ env["REQUEST_METHOD"].should.equal "POST"
+ env["QUERY_STRING"].should.equal ""
+ env["PATH_INFO"].should.equal "/foo"
+ env["CONTENT_TYPE"].should.equal "multipart/form-data; boundary=AaB03x"
+ env["mock.postdata"].length.should.equal 206
+ end
+
+ specify "should behave valid according to the Rack spec" do
+ lambda {
+ res = Rack::MockRequest.new(app).
+ get("https://bla.example.org:9292/meh/foo?bar", :lint => true)
+ }.should.not.raise(Rack::Lint::LintError)
+ end
+end
+
+context "Rack::MockResponse" do
+ specify "should provide access to the HTTP status" do
+ res = Rack::MockRequest.new(app).get("")
+ res.should.be.successful
+ res.should.be.ok
+
+ res = Rack::MockRequest.new(app).get("/?status=404")
+ res.should.not.be.successful
+ res.should.be.client_error
+ res.should.be.not_found
+
+ res = Rack::MockRequest.new(app).get("/?status=501")
+ res.should.not.be.successful
+ res.should.be.server_error
+
+ res = Rack::MockRequest.new(app).get("/?status=307")
+ res.should.be.redirect
+
+ res = Rack::MockRequest.new(app).get("/?status=201", :lint => true)
+ res.should.be.empty
+ end
+
+ specify "should provide access to the HTTP headers" do
+ res = Rack::MockRequest.new(app).get("")
+ res.should.include "Content-Type"
+ res.headers["Content-Type"].should.equal "text/yaml"
+ res.original_headers["Content-Type"].should.equal "text/yaml"
+ res["Content-Type"].should.equal "text/yaml"
+ res.content_type.should.equal "text/yaml"
+ res.content_length.should.be 414 # needs change often.
+ res.location.should.be.nil
+ end
+
+ specify "should provide access to the HTTP body" do
+ res = Rack::MockRequest.new(app).get("")
+ res.body.should =~ /rack/
+ res.should =~ /rack/
+ res.should.match(/rack/)
+ res.should.satisfy { |r| r.match(/rack/) }
+ end
+
+ specify "should provide access to the Rack errors" do
+ res = Rack::MockRequest.new(app).get("/?error=foo", :lint => true)
+ res.should.be.ok
+ res.errors.should.not.be.empty
+ res.errors.should.include "foo"
+ end
+
+ specify "should optionally make Rack errors fatal" do
+ lambda {
+ Rack::MockRequest.new(app).get("/?error=foo", :fatal => true)
+ }.should.raise(Rack::MockRequest::FatalWarning)
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_mongrel.rb b/vendor/gems/rack-1.1.0/test/spec_rack_mongrel.rb
new file mode 100644
index 00000000..4b386891
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_mongrel.rb
@@ -0,0 +1,189 @@
+require 'test/spec'
+
+begin
+require 'rack/handler/mongrel'
+require 'rack/urlmap'
+require 'rack/lint'
+require 'testrequest'
+require 'timeout'
+
+Thread.abort_on_exception = true
+$tcp_defer_accept_opts = nil
+$tcp_cork_opts = nil
+
+context "Rack::Handler::Mongrel" do
+ include TestRequest::Helpers
+
+ setup do
+ server = Mongrel::HttpServer.new(@host='0.0.0.0', @port=9201)
+ server.register('/test',
+ Rack::Handler::Mongrel.new(Rack::Lint.new(TestRequest.new)))
+ server.register('/stream',
+ Rack::Handler::Mongrel.new(Rack::Lint.new(StreamingRequest)))
+ @acc = server.run
+ end
+
+ specify "should respond" do
+ lambda {
+ GET("/test")
+ }.should.not.raise
+ end
+
+ specify "should be a Mongrel" do
+ GET("/test")
+ status.should.be 200
+ response["SERVER_SOFTWARE"].should =~ /Mongrel/
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
+ response["SERVER_PORT"].should.equal "9201"
+ response["SERVER_NAME"].should.equal "0.0.0.0"
+ end
+
+ specify "should have rack headers" do
+ GET("/test")
+ response["rack.version"].should.equal [1,1]
+ response["rack.multithread"].should.be true
+ response["rack.multiprocess"].should.be false
+ response["rack.run_once"].should.be false
+ end
+
+ specify "should have CGI headers on GET" do
+ GET("/test")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/test"
+ response["PATH_INFO"].should.be.equal ""
+ response["QUERY_STRING"].should.equal ""
+ response["test.postdata"].should.equal ""
+
+ GET("/test/foo?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/test/foo"
+ response["PATH_INFO"].should.equal "/foo"
+ response["QUERY_STRING"].should.equal "quux=1"
+ end
+
+ specify "should have CGI headers on POST" do
+ POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+ status.should.equal 200
+ response["REQUEST_METHOD"].should.equal "POST"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/test"
+ response["QUERY_STRING"].should.equal ""
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
+ response["test.postdata"].should.equal "rack-form-data=23"
+ end
+
+ specify "should support HTTP auth" do
+ GET("/test", {:user => "ruth", :passwd => "secret"})
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
+ end
+
+ specify "should set status" do
+ GET("/test?secret")
+ status.should.equal 403
+ response["rack.url_scheme"].should.equal "http"
+ end
+
+ specify "should provide a .run" do
+ block_ran = false
+ Thread.new {
+ Rack::Handler::Mongrel.run(lambda {}, {:Port => 9211}) { |server|
+ server.should.be.kind_of Mongrel::HttpServer
+ block_ran = true
+ }
+ }
+ sleep 1
+ block_ran.should.be true
+ end
+
+ specify "should provide a .run that maps a hash" do
+ block_ran = false
+ Thread.new {
+ map = {'/'=>lambda{},'/foo'=>lambda{}}
+ Rack::Handler::Mongrel.run(map, :map => true, :Port => 9221) { |server|
+ server.should.be.kind_of Mongrel::HttpServer
+ server.classifier.uris.size.should.be 2
+ server.classifier.uris.should.not.include '/arf'
+ server.classifier.uris.should.include '/'
+ server.classifier.uris.should.include '/foo'
+ block_ran = true
+ }
+ }
+ sleep 1
+ block_ran.should.be true
+ end
+
+ specify "should provide a .run that maps a urlmap" do
+ block_ran = false
+ Thread.new {
+ map = Rack::URLMap.new({'/'=>lambda{},'/bar'=>lambda{}})
+ Rack::Handler::Mongrel.run(map, {:map => true, :Port => 9231}) { |server|
+ server.should.be.kind_of Mongrel::HttpServer
+ server.classifier.uris.size.should.be 2
+ server.classifier.uris.should.not.include '/arf'
+ server.classifier.uris.should.include '/'
+ server.classifier.uris.should.include '/bar'
+ block_ran = true
+ }
+ }
+ sleep 1
+ block_ran.should.be true
+ end
+
+ specify "should provide a .run that maps a urlmap restricting by host" do
+ block_ran = false
+ Thread.new {
+ map = Rack::URLMap.new({
+ '/' => lambda{},
+ '/foo' => lambda{},
+ '/bar' => lambda{},
+ 'http://localhost/' => lambda{},
+ 'http://localhost/bar' => lambda{},
+ 'http://falsehost/arf' => lambda{},
+ 'http://falsehost/qux' => lambda{}
+ })
+ opt = {:map => true, :Port => 9241, :Host => 'localhost'}
+ Rack::Handler::Mongrel.run(map, opt) { |server|
+ server.should.be.kind_of Mongrel::HttpServer
+ server.classifier.uris.should.include '/'
+ server.classifier.handler_map['/'].size.should.be 2
+ server.classifier.uris.should.include '/foo'
+ server.classifier.handler_map['/foo'].size.should.be 1
+ server.classifier.uris.should.include '/bar'
+ server.classifier.handler_map['/bar'].size.should.be 2
+ server.classifier.uris.should.not.include '/qux'
+ server.classifier.uris.should.not.include '/arf'
+ server.classifier.uris.size.should.be 3
+ block_ran = true
+ }
+ }
+ sleep 1
+ block_ran.should.be true
+ end
+
+ specify "should stream #each part of the response" do
+ body = ''
+ begin
+ Timeout.timeout(1) do
+ Net::HTTP.start(@host, @port) do |http|
+ get = Net::HTTP::Get.new('/stream')
+ http.request(get) do |response|
+ response.read_body { |part| body << part }
+ end
+ end
+ end
+ rescue Timeout::Error
+ end
+ body.should.not.be.empty
+ end
+
+ teardown do
+ @acc.raise Mongrel::StopServer
+ end
+end
+
+rescue LoadError
+ $stderr.puts "Skipping Rack::Handler::Mongrel tests (Mongrel is required). `gem install mongrel` and try again."
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_nulllogger.rb b/vendor/gems/rack-1.1.0/test/spec_rack_nulllogger.rb
new file mode 100644
index 00000000..b3c2bc9c
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_nulllogger.rb
@@ -0,0 +1,13 @@
+require 'rack/nulllogger'
+require 'rack/lint'
+require 'rack/mock'
+
+context "Rack::NullLogger" do
+ specify "acks as a nop logger" do
+ app = lambda { |env|
+ env['rack.logger'].warn "b00m"
+ [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]]
+ }
+ Rack::NullLogger.new(app).call({})
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_recursive.rb b/vendor/gems/rack-1.1.0/test/spec_rack_recursive.rb
new file mode 100644
index 00000000..afc1a0d9
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_recursive.rb
@@ -0,0 +1,77 @@
+require 'test/spec'
+
+require 'rack/recursive'
+require 'rack/urlmap'
+require 'rack/response'
+require 'rack/mock'
+
+context "Rack::Recursive" do
+ setup do
+
+ @app1 = lambda { |env|
+ res = Rack::Response.new
+ res["X-Path-Info"] = env["PATH_INFO"]
+ res["X-Query-String"] = env["QUERY_STRING"]
+ res.finish do |res|
+ res.write "App1"
+ end
+ }
+
+ @app2 = lambda { |env|
+ Rack::Response.new.finish do |res|
+ res.write "App2"
+ _, _, body = env['rack.recursive.include'].call(env, "/app1")
+ body.each { |b|
+ res.write b
+ }
+ end
+ }
+
+ @app3 = lambda { |env|
+ raise Rack::ForwardRequest.new("/app1")
+ }
+
+ @app4 = lambda { |env|
+ raise Rack::ForwardRequest.new("http://example.org/app1/quux?meh")
+ }
+
+ end
+
+ specify "should allow for subrequests" do
+ res = Rack::MockRequest.new(Rack::Recursive.new(
+ Rack::URLMap.new("/app1" => @app1,
+ "/app2" => @app2))).
+ get("/app2")
+
+ res.should.be.ok
+ res.body.should.equal "App2App1"
+ end
+
+ specify "should raise error on requests not below the app" do
+ app = Rack::URLMap.new("/app1" => @app1,
+ "/app" => Rack::Recursive.new(
+ Rack::URLMap.new("/1" => @app1,
+ "/2" => @app2)))
+
+ lambda {
+ Rack::MockRequest.new(app).get("/app/2")
+ }.should.raise(ArgumentError).
+ message.should =~ /can only include below/
+ end
+
+ specify "should support forwarding" do
+ app = Rack::Recursive.new(Rack::URLMap.new("/app1" => @app1,
+ "/app3" => @app3,
+ "/app4" => @app4))
+
+ res = Rack::MockRequest.new(app).get("/app3")
+ res.should.be.ok
+ res.body.should.equal "App1"
+
+ res = Rack::MockRequest.new(app).get("/app4")
+ res.should.be.ok
+ res.body.should.equal "App1"
+ res["X-Path-Info"].should.equal "/quux"
+ res["X-Query-String"].should.equal "meh"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_request.rb b/vendor/gems/rack-1.1.0/test/spec_rack_request.rb
new file mode 100644
index 00000000..fcdeb484
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_request.rb
@@ -0,0 +1,545 @@
+require 'test/spec'
+require 'stringio'
+
+require 'rack/request'
+require 'rack/mock'
+
+context "Rack::Request" do
+ specify "wraps the rack variables" do
+ req = Rack::Request.new(Rack::MockRequest.env_for("http://example.com:8080/"))
+
+ req.body.should.respond_to? :gets
+ req.scheme.should.equal "http"
+ req.request_method.should.equal "GET"
+
+ req.should.be.get
+ req.should.not.be.post
+ req.should.not.be.put
+ req.should.not.be.delete
+ req.should.not.be.head
+
+ req.script_name.should.equal ""
+ req.path_info.should.equal "/"
+ req.query_string.should.equal ""
+
+ req.host.should.equal "example.com"
+ req.port.should.equal 8080
+
+ req.content_length.should.equal "0"
+ req.content_type.should.be.nil
+ end
+
+ specify "can figure out the correct host" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/", "HTTP_HOST" => "www2.example.org")
+ req.host.should.equal "www2.example.org"
+
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org", "SERVER_PORT" => "9292")
+ req.host.should.equal "example.org"
+
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292")
+ req.host.should.equal "example.org"
+
+ env = Rack::MockRequest.env_for("/", "SERVER_ADDR" => "192.168.1.1", "SERVER_PORT" => "9292")
+ env.delete("SERVER_NAME")
+ req = Rack::Request.new(env)
+ req.host.should.equal "192.168.1.1"
+
+ env = Rack::MockRequest.env_for("/")
+ env.delete("SERVER_NAME")
+ req = Rack::Request.new(env)
+ req.host.should.equal ""
+ end
+
+ specify "can parse the query string" do
+ req = Rack::Request.new(Rack::MockRequest.env_for("/?foo=bar&quux=bla"))
+ req.query_string.should.equal "foo=bar&quux=bla"
+ req.GET.should.equal "foo" => "bar", "quux" => "bla"
+ req.POST.should.be.empty
+ req.params.should.equal "foo" => "bar", "quux" => "bla"
+ end
+
+ specify "raises if rack.input is missing" do
+ req = Rack::Request.new({})
+ lambda { req.POST }.should.raise(RuntimeError)
+ end
+
+ specify "can parse POST data when method is POST and no Content-Type given" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/?foo=quux",
+ "REQUEST_METHOD" => 'POST',
+ :input => "foo=bar&quux=bla")
+ req.content_type.should.be.nil
+ req.media_type.should.be.nil
+ req.query_string.should.equal "foo=quux"
+ req.GET.should.equal "foo" => "quux"
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ req.params.should.equal "foo" => "bar", "quux" => "bla"
+ end
+
+ specify "can parse POST data with explicit content type regardless of method" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded;foo=bar',
+ :input => "foo=bar&quux=bla")
+ req.content_type.should.equal 'application/x-www-form-urlencoded;foo=bar'
+ req.media_type.should.equal 'application/x-www-form-urlencoded'
+ req.media_type_params['foo'].should.equal 'bar'
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ req.params.should.equal "foo" => "bar", "quux" => "bla"
+ end
+
+ specify "does not parse POST data when media type is not form-data" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/?foo=quux",
+ "REQUEST_METHOD" => 'POST',
+ "CONTENT_TYPE" => 'text/plain;charset=utf-8',
+ :input => "foo=bar&quux=bla")
+ req.content_type.should.equal 'text/plain;charset=utf-8'
+ req.media_type.should.equal 'text/plain'
+ req.media_type_params['charset'].should.equal 'utf-8'
+ req.POST.should.be.empty
+ req.params.should.equal "foo" => "quux"
+ req.body.read.should.equal "foo=bar&quux=bla"
+ end
+
+ specify "can parse POST data on PUT when media type is form-data" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/?foo=quux",
+ "REQUEST_METHOD" => 'PUT',
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded',
+ :input => "foo=bar&quux=bla")
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ req.body.read.should.equal "foo=bar&quux=bla"
+ end
+
+ specify "rewinds input after parsing POST data" do
+ input = StringIO.new("foo=bar&quux=bla")
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded;foo=bar',
+ :input => input)
+ req.params.should.equal "foo" => "bar", "quux" => "bla"
+ input.read.should.equal "foo=bar&quux=bla"
+ end
+
+ specify "cleans up Safari's ajax POST body" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/",
+ 'REQUEST_METHOD' => 'POST', :input => "foo=bar&quux=bla\0")
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ end
+
+ specify "can get value by key from params with #[]" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("?foo=quux")
+ req['foo'].should.equal 'quux'
+ req[:foo].should.equal 'quux'
+ end
+
+ specify "can set value to key on params with #[]=" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("?foo=duh")
+ req['foo'].should.equal 'duh'
+ req[:foo].should.equal 'duh'
+ req.params.should.equal 'foo' => 'duh'
+
+ req['foo'] = 'bar'
+ req.params.should.equal 'foo' => 'bar'
+ req['foo'].should.equal 'bar'
+ req[:foo].should.equal 'bar'
+
+ req[:foo] = 'jaz'
+ req.params.should.equal 'foo' => 'jaz'
+ req['foo'].should.equal 'jaz'
+ req[:foo].should.equal 'jaz'
+ end
+
+ specify "values_at answers values by keys in order given" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("?foo=baz&wun=der&bar=ful")
+ req.values_at('foo').should.equal ['baz']
+ req.values_at('foo', 'wun').should.equal ['baz', 'der']
+ req.values_at('bar', 'foo', 'wun').should.equal ['ful', 'baz', 'der']
+ end
+
+ specify "referrer should be extracted correct" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/", "HTTP_REFERER" => "/some/path")
+ req.referer.should.equal "/some/path"
+
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/")
+ req.referer.should.equal "/"
+ end
+
+ specify "user agent should be extracted correct" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/", "HTTP_USER_AGENT" => "Mozilla/4.0 (compatible)")
+ req.user_agent.should.equal "Mozilla/4.0 (compatible)"
+
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/")
+ req.user_agent.should.equal nil
+ end
+
+ specify "can cache, but invalidates the cache" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/?foo=quux",
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
+ :input => "foo=bar&quux=bla")
+ req.GET.should.equal "foo" => "quux"
+ req.GET.should.equal "foo" => "quux"
+ req.env["QUERY_STRING"] = "bla=foo"
+ req.GET.should.equal "bla" => "foo"
+ req.GET.should.equal "bla" => "foo"
+
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ req.POST.should.equal "foo" => "bar", "quux" => "bla"
+ req.env["rack.input"] = StringIO.new("foo=bla&quux=bar")
+ req.POST.should.equal "foo" => "bla", "quux" => "bar"
+ req.POST.should.equal "foo" => "bla", "quux" => "bar"
+ end
+
+ specify "can figure out if called via XHR" do
+ req = Rack::Request.new(Rack::MockRequest.env_for(""))
+ req.should.not.be.xhr
+
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("", "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")
+ req.should.be.xhr
+ end
+
+ specify "can parse cookies" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m")
+ req.cookies.should.equal "foo" => "bar", "quux" => "h&m"
+ req.cookies.should.equal "foo" => "bar", "quux" => "h&m"
+ req.env.delete("HTTP_COOKIE")
+ req.cookies.should.equal({})
+ end
+
+ specify "parses cookies according to RFC 2109" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for('', 'HTTP_COOKIE' => 'foo=bar;foo=car')
+ req.cookies.should.equal 'foo' => 'bar'
+ end
+
+ specify "provides setters" do
+ req = Rack::Request.new(e=Rack::MockRequest.env_for(""))
+ req.script_name.should.equal ""
+ req.script_name = "/foo"
+ req.script_name.should.equal "/foo"
+ e["SCRIPT_NAME"].should.equal "/foo"
+
+ req.path_info.should.equal "/"
+ req.path_info = "/foo"
+ req.path_info.should.equal "/foo"
+ e["PATH_INFO"].should.equal "/foo"
+ end
+
+ specify "provides the original env" do
+ req = Rack::Request.new(e=Rack::MockRequest.env_for(""))
+ req.env.should.be e
+ end
+
+ specify "can restore the URL" do
+ Rack::Request.new(Rack::MockRequest.env_for("")).url.
+ should.equal "http://example.org/"
+ Rack::Request.new(Rack::MockRequest.env_for("", "SCRIPT_NAME" => "/foo")).url.
+ should.equal "http://example.org/foo/"
+ Rack::Request.new(Rack::MockRequest.env_for("/foo")).url.
+ should.equal "http://example.org/foo"
+ Rack::Request.new(Rack::MockRequest.env_for("?foo")).url.
+ should.equal "http://example.org/?foo"
+ Rack::Request.new(Rack::MockRequest.env_for("http://example.org:8080/")).url.
+ should.equal "http://example.org:8080/"
+ Rack::Request.new(Rack::MockRequest.env_for("https://example.org/")).url.
+ should.equal "https://example.org/"
+
+ Rack::Request.new(Rack::MockRequest.env_for("https://example.com:8080/foo?foo")).url.
+ should.equal "https://example.com:8080/foo?foo"
+ end
+
+ specify "can restore the full path" do
+ Rack::Request.new(Rack::MockRequest.env_for("")).fullpath.
+ should.equal "/"
+ Rack::Request.new(Rack::MockRequest.env_for("", "SCRIPT_NAME" => "/foo")).fullpath.
+ should.equal "/foo/"
+ Rack::Request.new(Rack::MockRequest.env_for("/foo")).fullpath.
+ should.equal "/foo"
+ Rack::Request.new(Rack::MockRequest.env_for("?foo")).fullpath.
+ should.equal "/?foo"
+ Rack::Request.new(Rack::MockRequest.env_for("http://example.org:8080/")).fullpath.
+ should.equal "/"
+ Rack::Request.new(Rack::MockRequest.env_for("https://example.org/")).fullpath.
+ should.equal "/"
+
+ Rack::Request.new(Rack::MockRequest.env_for("https://example.com:8080/foo?foo")).fullpath.
+ should.equal "/foo?foo"
+ end
+
+ specify "can handle multiple media type parameters" do
+ req = Rack::Request.new \
+ Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => 'text/plain; foo=BAR,baz=bizzle dizzle;BLING=bam')
+ req.should.not.be.form_data
+ req.media_type_params.should.include 'foo'
+ req.media_type_params['foo'].should.equal 'BAR'
+ req.media_type_params.should.include 'baz'
+ req.media_type_params['baz'].should.equal 'bizzle dizzle'
+ req.media_type_params.should.not.include 'BLING'
+ req.media_type_params.should.include 'bling'
+ req.media_type_params['bling'].should.equal 'bam'
+ end
+
+ specify "can parse multipart form data" do
+ # Adapted from RFC 1867.
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ req.POST.should.include "fileupload"
+ req.POST.should.include "reply"
+
+ req.should.be.form_data
+ req.content_length.should.equal input.size
+ req.media_type.should.equal 'multipart/form-data'
+ req.media_type_params.should.include 'boundary'
+ req.media_type_params['boundary'].should.equal 'AaB03x'
+
+ req.POST["reply"].should.equal "yes"
+
+ f = req.POST["fileupload"]
+ f.should.be.kind_of Hash
+ f[:type].should.equal "image/jpeg"
+ f[:filename].should.equal "dj.jpg"
+ f.should.include :tempfile
+ f[:tempfile].size.should.equal 76
+ end
+
+ specify "can parse big multipart form data" do
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ req.POST["huge"][:tempfile].size.should.equal 32768
+ req.POST["mean"][:tempfile].size.should.equal 10
+ req.POST["mean"][:tempfile].read.should.equal "--AaB03xha"
+ end
+
+ specify "can detect invalid multipart form data" do
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ lambda { req.POST }.should.raise(EOFError)
+
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ lambda { req.POST }.should.raise(EOFError)
+
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ lambda { req.POST }.should.raise(EOFError)
+ end
+
+ specify "shouldn't try to interpret binary as utf8" do
+ begin
+ original_kcode = $KCODE
+ $KCODE='UTF8'
+
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ lambda{req.POST}.should.not.raise(EOFError)
+ req.POST["fileupload"][:tempfile].size.should.equal 4
+ ensure
+ $KCODE = original_kcode
+ end
+ end
+
+
+ specify "should work around buggy 1.8.* Tempfile equality" do
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => rack_input)
+
+ lambda {req.POST}.should.not.raise
+ lambda {req.POST}.should.blaming("input re-processed!").not.raise
+ end
+
+ specify "does conform to the Rack spec" do
+ app = lambda { |env|
+ content = Rack::Request.new(env).POST["file"].inspect
+ size = content.respond_to?(:bytesize) ? content.bytesize : content.size
+ [200, {"Content-Type" => "text/html", "Content-Length" => size.to_s}, [content]]
+ }
+
+ input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size.to_s, "rack.input" => StringIO.new(input)
+
+ res.should.be.ok
+ end
+
+ specify "should parse Accept-Encoding correctly" do
+ parser = lambda do |x|
+ Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => x)).accept_encoding
+ end
+
+ parser.call(nil).should.equal([])
+
+ parser.call("compress, gzip").should.equal([["compress", 1.0], ["gzip", 1.0]])
+ parser.call("").should.equal([])
+ parser.call("*").should.equal([["*", 1.0]])
+ parser.call("compress;q=0.5, gzip;q=1.0").should.equal([["compress", 0.5], ["gzip", 1.0]])
+ parser.call("gzip;q=1.0, identity; q=0.5, *;q=0").should.equal([["gzip", 1.0], ["identity", 0.5], ["*", 0] ])
+
+ lambda { parser.call("gzip ; q=1.0") }.should.raise(RuntimeError)
+ end
+
+ specify 'should provide ip information' do
+ app = lambda { |env|
+ request = Rack::Request.new(env)
+ response = Rack::Response.new
+ response.write request.ip
+ response.finish
+ }
+
+ mock = Rack::MockRequest.new(Rack::Lint.new(app))
+ res = mock.get '/', 'REMOTE_ADDR' => '123.123.123.123'
+ res.body.should.equal '123.123.123.123'
+
+ res = mock.get '/',
+ 'REMOTE_ADDR' => '123.123.123.123',
+ 'HTTP_X_FORWARDED_FOR' => '234.234.234.234'
+
+ res.body.should.equal '234.234.234.234'
+
+ res = mock.get '/',
+ 'REMOTE_ADDR' => '123.123.123.123',
+ 'HTTP_X_FORWARDED_FOR' => '234.234.234.234,212.212.212.212'
+
+ res.body.should.equal '212.212.212.212'
+ end
+
+ class MyRequest < Rack::Request
+ def params
+ {:foo => "bar"}
+ end
+ end
+
+ specify "should allow subclass request to be instantiated after parent request" do
+ env = Rack::MockRequest.env_for("/?foo=bar")
+
+ req1 = Rack::Request.new(env)
+ req1.GET.should.equal "foo" => "bar"
+ req1.params.should.equal "foo" => "bar"
+
+ req2 = MyRequest.new(env)
+ req2.GET.should.equal "foo" => "bar"
+ req2.params.should.equal :foo => "bar"
+ end
+
+ specify "should allow parent request to be instantiated after subclass request" do
+ env = Rack::MockRequest.env_for("/?foo=bar")
+
+ req1 = MyRequest.new(env)
+ req1.GET.should.equal "foo" => "bar"
+ req1.params.should.equal :foo => "bar"
+
+ req2 = Rack::Request.new(env)
+ req2.GET.should.equal "foo" => "bar"
+ req2.params.should.equal "foo" => "bar"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_response.rb b/vendor/gems/rack-1.1.0/test/spec_rack_response.rb
new file mode 100644
index 00000000..7989013d
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_response.rb
@@ -0,0 +1,221 @@
+require 'test/spec'
+require 'set'
+
+require 'rack/response'
+
+context "Rack::Response" do
+ specify "has sensible default values" do
+ response = Rack::Response.new
+ status, header, body = response.finish
+ status.should.equal 200
+ header.should.equal "Content-Type" => "text/html"
+ body.each { |part|
+ part.should.equal ""
+ }
+
+ response = Rack::Response.new
+ status, header, body = *response
+ status.should.equal 200
+ header.should.equal "Content-Type" => "text/html"
+ body.each { |part|
+ part.should.equal ""
+ }
+ end
+
+ specify "can be written to" do
+ response = Rack::Response.new
+
+ status, header, body = response.finish do
+ response.write "foo"
+ response.write "bar"
+ response.write "baz"
+ end
+
+ parts = []
+ body.each { |part| parts << part }
+
+ parts.should.equal ["foo", "bar", "baz"]
+ end
+
+ specify "can set and read headers" do
+ response = Rack::Response.new
+ response["Content-Type"].should.equal "text/html"
+ response["Content-Type"] = "text/plain"
+ response["Content-Type"].should.equal "text/plain"
+ end
+
+ specify "can set cookies" do
+ response = Rack::Response.new
+
+ response.set_cookie "foo", "bar"
+ response["Set-Cookie"].should.equal "foo=bar"
+ response.set_cookie "foo2", "bar2"
+ response["Set-Cookie"].should.equal ["foo=bar", "foo2=bar2"]
+ response.set_cookie "foo3", "bar3"
+ response["Set-Cookie"].should.equal ["foo=bar", "foo2=bar2", "foo3=bar3"]
+ end
+
+ specify "formats the Cookie expiration date accordingly to RFC 2109" do
+ response = Rack::Response.new
+
+ response.set_cookie "foo", {:value => "bar", :expires => Time.now+10}
+ response["Set-Cookie"].should.match(
+ /expires=..., \d\d-...-\d\d\d\d \d\d:\d\d:\d\d .../)
+ end
+
+ specify "can set secure cookies" do
+ response = Rack::Response.new
+ response.set_cookie "foo", {:value => "bar", :secure => true}
+ response["Set-Cookie"].should.equal "foo=bar; secure"
+ end
+
+ specify "can set http only cookies" do
+ response = Rack::Response.new
+ response.set_cookie "foo", {:value => "bar", :httponly => true}
+ response["Set-Cookie"].should.equal "foo=bar; HttpOnly"
+ end
+
+ specify "can delete cookies" do
+ response = Rack::Response.new
+ response.set_cookie "foo", "bar"
+ response.set_cookie "foo2", "bar2"
+ response.delete_cookie "foo"
+ response["Set-Cookie"].should.equal ["foo2=bar2",
+ "foo=; expires=Thu, 01-Jan-1970 00:00:00 GMT"]
+ end
+
+ specify "can do redirects" do
+ response = Rack::Response.new
+ response.redirect "/foo"
+ status, header, body = response.finish
+
+ status.should.equal 302
+ header["Location"].should.equal "/foo"
+
+ response = Rack::Response.new
+ response.redirect "/foo", 307
+ status, header, body = response.finish
+
+ status.should.equal 307
+ end
+
+ specify "has a useful constructor" do
+ r = Rack::Response.new("foo")
+ status, header, body = r.finish
+ str = ""; body.each { |part| str << part }
+ str.should.equal "foo"
+
+ r = Rack::Response.new(["foo", "bar"])
+ status, header, body = r.finish
+ str = ""; body.each { |part| str << part }
+ str.should.equal "foobar"
+
+ r = Rack::Response.new(["foo", "bar"].to_set)
+ r.write "foo"
+ status, header, body = r.finish
+ str = ""; body.each { |part| str << part }
+ str.should.equal "foobarfoo"
+
+ r = Rack::Response.new([], 500)
+ r.status.should.equal 500
+
+ r = Rack::Response.new([], "200 OK")
+ r.status.should.equal 200
+ end
+
+ specify "has a constructor that can take a block" do
+ r = Rack::Response.new { |res|
+ res.status = 404
+ res.write "foo"
+ }
+ status, header, body = r.finish
+ str = ""; body.each { |part| str << part }
+ str.should.equal "foo"
+ status.should.equal 404
+ end
+
+ specify "doesn't return invalid responses" do
+ r = Rack::Response.new(["foo", "bar"], 204)
+ status, header, body = r.finish
+ str = ""; body.each { |part| str << part }
+ str.should.be.empty
+ header["Content-Type"].should.equal nil
+
+ lambda {
+ Rack::Response.new(Object.new)
+ }.should.raise(TypeError).
+ message.should =~ /stringable or iterable required/
+ end
+
+ specify "knows if it's empty" do
+ r = Rack::Response.new
+ r.should.be.empty
+ r.write "foo"
+ r.should.not.be.empty
+
+ r = Rack::Response.new
+ r.should.be.empty
+ r.finish
+ r.should.be.empty
+
+ r = Rack::Response.new
+ r.should.be.empty
+ r.finish { }
+ r.should.not.be.empty
+ end
+
+ specify "should provide access to the HTTP status" do
+ res = Rack::Response.new
+ res.status = 200
+ res.should.be.successful
+ res.should.be.ok
+
+ res.status = 404
+ res.should.not.be.successful
+ res.should.be.client_error
+ res.should.be.not_found
+
+ res.status = 501
+ res.should.not.be.successful
+ res.should.be.server_error
+
+ res.status = 307
+ res.should.be.redirect
+ end
+
+ specify "should provide access to the HTTP headers" do
+ res = Rack::Response.new
+ res["Content-Type"] = "text/yaml"
+
+ res.should.include "Content-Type"
+ res.headers["Content-Type"].should.equal "text/yaml"
+ res["Content-Type"].should.equal "text/yaml"
+ res.content_type.should.equal "text/yaml"
+ res.content_length.should.be.nil
+ res.location.should.be.nil
+ end
+
+ specify "does not add or change Content-Length when #finish()ing" do
+ res = Rack::Response.new
+ res.status = 200
+ res.finish
+ res.headers["Content-Length"].should.be.nil
+
+ res = Rack::Response.new
+ res.status = 200
+ res.headers["Content-Length"] = "10"
+ res.finish
+ res.headers["Content-Length"].should.equal "10"
+ end
+
+ specify "updates Content-Length when body appended to using #write" do
+ res = Rack::Response.new
+ res.status = 200
+ res.headers["Content-Length"].should.be.nil
+ res.write "Hi"
+ res.headers["Content-Length"].should.equal "2"
+ res.write " there"
+ res.headers["Content-Length"].should.equal "8"
+ end
+
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_rewindable_input.rb b/vendor/gems/rack-1.1.0/test/spec_rack_rewindable_input.rb
new file mode 100644
index 00000000..78bebfc9
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_rewindable_input.rb
@@ -0,0 +1,118 @@
+require 'test/spec'
+require 'stringio'
+require 'rack/rewindable_input'
+
+shared_context "a rewindable IO object" do
+ setup do
+ @rio = Rack::RewindableInput.new(@io)
+ end
+
+ teardown do
+ @rio.close
+ end
+
+ specify "should be able to handle to read()" do
+ @rio.read.should.equal "hello world"
+ end
+
+ specify "should be able to handle to read(nil)" do
+ @rio.read(nil).should.equal "hello world"
+ end
+
+ specify "should be able to handle to read(length)" do
+ @rio.read(1).should.equal "h"
+ end
+
+ specify "should be able to handle to read(length, buffer)" do
+ buffer = ""
+ result = @rio.read(1, buffer)
+ result.should.equal "h"
+ result.object_id.should.equal buffer.object_id
+ end
+
+ specify "should be able to handle to read(nil, buffer)" do
+ buffer = ""
+ result = @rio.read(nil, buffer)
+ result.should.equal "hello world"
+ result.object_id.should.equal buffer.object_id
+ end
+
+ specify "should rewind to the beginning when #rewind is called" do
+ @rio.read(1)
+ @rio.rewind
+ @rio.read.should.equal "hello world"
+ end
+
+ specify "should be able to handle gets" do
+ @rio.gets.should == "hello world"
+ end
+
+ specify "should be able to handle each" do
+ array = []
+ @rio.each do |data|
+ array << data
+ end
+ array.should.equal(["hello world"])
+ end
+
+ specify "should not buffer into a Tempfile if no data has been read yet" do
+ @rio.instance_variable_get(:@rewindable_io).should.be.nil
+ end
+
+ specify "should buffer into a Tempfile when data has been consumed for the first time" do
+ @rio.read(1)
+ tempfile = @rio.instance_variable_get(:@rewindable_io)
+ tempfile.should.not.be.nil
+ @rio.read(1)
+ tempfile2 = @rio.instance_variable_get(:@rewindable_io)
+ tempfile2.should.equal tempfile
+ end
+
+ specify "should close the underlying tempfile upon calling #close" do
+ @rio.read(1)
+ tempfile = @rio.instance_variable_get(:@rewindable_io)
+ @rio.close
+ tempfile.should.be.closed
+ end
+
+ specify "should be possibel to call #close when no data has been buffered yet" do
+ @rio.close
+ end
+
+ specify "should be possible to call #close multiple times" do
+ @rio.close
+ @rio.close
+ end
+end
+
+context "Rack::RewindableInput" do
+ context "given an IO object that is already rewindable" do
+ setup do
+ @io = StringIO.new("hello world")
+ end
+
+ it_should_behave_like "a rewindable IO object"
+ end
+
+ context "given an IO object that is not rewindable" do
+ setup do
+ @io = StringIO.new("hello world")
+ @io.instance_eval do
+ undef :rewind
+ end
+ end
+
+ it_should_behave_like "a rewindable IO object"
+ end
+
+ context "given an IO object whose rewind method raises Errno::ESPIPE" do
+ setup do
+ @io = StringIO.new("hello world")
+ def @io.rewind
+ raise Errno::ESPIPE, "You can't rewind this!"
+ end
+ end
+
+ it_should_behave_like "a rewindable IO object"
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_runtime.rb b/vendor/gems/rack-1.1.0/test/spec_rack_runtime.rb
new file mode 100644
index 00000000..62d80956
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_runtime.rb
@@ -0,0 +1,35 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/runtime'
+
+context "Rack::Runtime" do
+ specify "sets X-Runtime is none is set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+ response = Rack::Runtime.new(app).call({})
+ response[1]['X-Runtime'].should =~ /[\d\.]+/
+ end
+
+ specify "does not set the X-Runtime if it is already set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain', "X-Runtime" => "foobar"}, "Hello, World!"] }
+ response = Rack::Runtime.new(app).call({})
+ response[1]['X-Runtime'].should == "foobar"
+ end
+
+ specify "should allow a suffix to be set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+ response = Rack::Runtime.new(app, "Test").call({})
+ response[1]['X-Runtime-Test'].should =~ /[\d\.]+/
+ end
+
+ specify "should allow multiple timers to be set" do
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+ runtime1 = Rack::Runtime.new(app, "App")
+ runtime2 = Rack::Runtime.new(runtime1, "All")
+ response = runtime2.call({})
+
+ response[1]['X-Runtime-App'].should =~ /[\d\.]+/
+ response[1]['X-Runtime-All'].should =~ /[\d\.]+/
+
+ Float(response[1]['X-Runtime-All']).should > Float(response[1]['X-Runtime-App'])
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_sendfile.rb b/vendor/gems/rack-1.1.0/test/spec_rack_sendfile.rb
new file mode 100644
index 00000000..8cfe2017
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_sendfile.rb
@@ -0,0 +1,86 @@
+require 'test/spec'
+require 'rack/mock'
+require 'rack/sendfile'
+
+context "Rack::File" do
+ specify "should respond to #to_path" do
+ Rack::File.new(Dir.pwd).should.respond_to :to_path
+ end
+end
+
+context "Rack::Sendfile" do
+ def sendfile_body
+ res = ['Hello World']
+ def res.to_path ; "/tmp/hello.txt" ; end
+ res
+ end
+
+ def simple_app(body=sendfile_body)
+ lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+ end
+
+ def sendfile_app(body=sendfile_body)
+ Rack::Sendfile.new(simple_app(body))
+ end
+
+ setup do
+ @request = Rack::MockRequest.new(sendfile_app)
+ end
+
+ def request(headers={})
+ yield @request.get('/', headers)
+ end
+
+ specify "does nothing when no X-Sendfile-Type header present" do
+ request do |response|
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ response.headers.should.not.include 'X-Sendfile'
+ end
+ end
+
+ specify "sets X-Sendfile response header and discards body" do
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+ response.should.be.ok
+ response.body.should.be.empty
+ response.headers['X-Sendfile'].should.equal '/tmp/hello.txt'
+ end
+ end
+
+ specify "sets X-Lighttpd-Send-File response header and discards body" do
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response|
+ response.should.be.ok
+ response.body.should.be.empty
+ response.headers['X-Lighttpd-Send-File'].should.equal '/tmp/hello.txt'
+ end
+ end
+
+ specify "sets X-Accel-Redirect response header and discards body" do
+ headers = {
+ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+ 'HTTP_X_ACCEL_MAPPING' => '/tmp/=/foo/bar/'
+ }
+ request headers do |response|
+ response.should.be.ok
+ response.body.should.be.empty
+ response.headers['X-Accel-Redirect'].should.equal '/foo/bar/hello.txt'
+ end
+ end
+
+ specify 'writes to rack.error when no X-Accel-Mapping is specified' do
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response|
+ response.should.be.ok
+ response.body.should.equal 'Hello World'
+ response.headers.should.not.include 'X-Accel-Redirect'
+ response.errors.should.include 'X-Accel-Mapping'
+ end
+ end
+
+ specify 'does nothing when body does not respond to #to_path' do
+ @request = Rack::MockRequest.new(sendfile_app(['Not a file...']))
+ request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+ response.body.should.equal 'Not a file...'
+ response.headers.should.not.include 'X-Sendfile'
+ end
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_session_cookie.rb b/vendor/gems/rack-1.1.0/test/spec_rack_session_cookie.rb
new file mode 100644
index 00000000..fba3f83b
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_session_cookie.rb
@@ -0,0 +1,73 @@
+require 'test/spec'
+
+require 'rack/session/cookie'
+require 'rack/mock'
+require 'rack/response'
+
+context "Rack::Session::Cookie" do
+ incrementor = lambda { |env|
+ env["rack.session"]["counter"] ||= 0
+ env["rack.session"]["counter"] += 1
+ Rack::Response.new(env["rack.session"].inspect).to_a
+ }
+
+ specify "creates a new cookie" do
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).get("/")
+ res["Set-Cookie"].should.match("rack.session=")
+ res.body.should.equal '{"counter"=>1}'
+ end
+
+ specify "loads from a cookie" do
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).get("/")
+ cookie = res["Set-Cookie"]
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).
+ get("/", "HTTP_COOKIE" => cookie)
+ res.body.should.equal '{"counter"=>2}'
+ cookie = res["Set-Cookie"]
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).
+ get("/", "HTTP_COOKIE" => cookie)
+ res.body.should.equal '{"counter"=>3}'
+ end
+
+ specify "survives broken cookies" do
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).
+ get("/", "HTTP_COOKIE" => "rack.session=blarghfasel")
+ res.body.should.equal '{"counter"=>1}'
+ end
+
+ bigcookie = lambda { |env|
+ env["rack.session"]["cookie"] = "big" * 3000
+ Rack::Response.new(env["rack.session"].inspect).to_a
+ }
+
+ specify "barks on too big cookies" do
+ lambda {
+ Rack::MockRequest.new(Rack::Session::Cookie.new(bigcookie)).
+ get("/", :fatal => true)
+ }.should.raise(Rack::MockRequest::FatalWarning)
+ end
+
+ specify "loads from a cookie wih integrity hash" do
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).get("/")
+ cookie = res["Set-Cookie"]
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).
+ get("/", "HTTP_COOKIE" => cookie)
+ res.body.should.equal '{"counter"=>2}'
+ cookie = res["Set-Cookie"]
+ res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor, :secret => 'test')).
+ get("/", "HTTP_COOKIE" => cookie)
+ res.body.should.equal '{"counter"=>3}'
+ end
+
+ specify "ignores tampered with session cookies" do
+ app = Rack::Session::Cookie.new(incrementor, :secret => 'test')
+ response1 = Rack::MockRequest.new(app).get("/")
+ _, digest = response1["Set-Cookie"].split("--")
+ tampered_with_cookie = "hackerman-was-here" + "--" + digest
+ response2 = Rack::MockRequest.new(app).get("/", "HTTP_COOKIE" =>
+ tampered_with_cookie)
+
+ # The tampered-with cookie is ignored, so we get back an identical Set-Cookie
+ response2["Set-Cookie"].should.equal(response1["Set-Cookie"])
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_session_memcache.rb b/vendor/gems/rack-1.1.0/test/spec_rack_session_memcache.rb
new file mode 100644
index 00000000..faac796e
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_session_memcache.rb
@@ -0,0 +1,273 @@
+require 'test/spec'
+
+begin
+ require 'rack/session/memcache'
+ require 'rack/mock'
+ require 'rack/response'
+ require 'thread'
+
+ context "Rack::Session::Memcache" do
+ session_key = Rack::Session::Memcache::DEFAULT_OPTIONS[:key]
+ session_match = /#{session_key}=([0-9a-fA-F]+);/
+ incrementor = lambda do |env|
+ env["rack.session"]["counter"] ||= 0
+ env["rack.session"]["counter"] += 1
+ Rack::Response.new(env["rack.session"].inspect).to_a
+ end
+ drop_session = proc do |env|
+ env['rack.session.options'][:drop] = true
+ incrementor.call(env)
+ end
+ renew_session = proc do |env|
+ env['rack.session.options'][:renew] = true
+ incrementor.call(env)
+ end
+ defer_session = proc do |env|
+ env['rack.session.options'][:defer] = true
+ incrementor.call(env)
+ end
+
+ specify "faults on no connection" do
+ if RUBY_VERSION < "1.9"
+ lambda do
+ Rack::Session::Memcache.new incrementor, :memcache_server => 'nosuchserver'
+ end.should.raise
+ else
+ lambda do
+ Rack::Session::Memcache.new incrementor, :memcache_server => 'nosuchserver'
+ end.should.raise ArgumentError
+ end
+ end
+
+ specify "connect to existing server" do
+ test_pool = MemCache.new incrementor, :namespace => 'test:rack:session'
+ end
+
+ specify "creates a new cookie" do
+ pool = Rack::Session::Memcache.new(incrementor)
+ res = Rack::MockRequest.new(pool).get("/")
+ res["Set-Cookie"].should.match("#{session_key}=")
+ res.body.should.equal '{"counter"=>1}'
+ end
+
+ specify "determines session from a cookie" do
+ pool = Rack::Session::Memcache.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ res = req.get("/")
+ cookie = res["Set-Cookie"]
+ req.get("/", "HTTP_COOKIE" => cookie).
+ body.should.equal '{"counter"=>2}'
+ req.get("/", "HTTP_COOKIE" => cookie).
+ body.should.equal '{"counter"=>3}'
+ end
+
+ specify "survives nonexistant cookies" do
+ bad_cookie = "rack.session=blarghfasel"
+ pool = Rack::Session::Memcache.new(incrementor)
+ res = Rack::MockRequest.new(pool).
+ get("/", "HTTP_COOKIE" => bad_cookie)
+ res.body.should.equal '{"counter"=>1}'
+ cookie = res["Set-Cookie"][session_match]
+ cookie.should.not.match(/#{bad_cookie}/)
+ end
+
+ specify "maintains freshness" do
+ pool = Rack::Session::Memcache.new(incrementor, :expire_after => 3)
+ res = Rack::MockRequest.new(pool).get('/')
+ res.body.should.include '"counter"=>1'
+ cookie = res["Set-Cookie"]
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
+ res["Set-Cookie"].should.equal cookie
+ res.body.should.include '"counter"=>2'
+ puts 'Sleeping to expire session' if $DEBUG
+ sleep 4
+ res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
+ res["Set-Cookie"].should.not.equal cookie
+ res.body.should.include '"counter"=>1'
+ end
+
+ specify "deletes cookies with :drop option" do
+ pool = Rack::Session::Memcache.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ drop = Rack::Utils::Context.new(pool, drop_session)
+ dreq = Rack::MockRequest.new(drop)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
+ res2["Set-Cookie"].should.equal nil
+ res2.body.should.equal '{"counter"=>3}'
+
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
+ res3["Set-Cookie"][session_match].should.not.equal session
+ res3.body.should.equal '{"counter"=>1}'
+ end
+
+ specify "provides new session id with :renew option" do
+ pool = Rack::Session::Memcache.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ renew = Rack::Utils::Context.new(pool, renew_session)
+ rreq = Rack::MockRequest.new(renew)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+
+ res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
+ new_cookie = res2["Set-Cookie"]
+ new_session = new_cookie[session_match]
+ new_session.should.not.equal session
+ res2.body.should.equal '{"counter"=>3}'
+
+ res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
+ res3["Set-Cookie"][session_match].should.equal new_session
+ res3.body.should.equal '{"counter"=>4}'
+ end
+
+ specify "omits cookie with :defer option" do
+ pool = Rack::Session::Memcache.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ defer = Rack::Utils::Context.new(pool, defer_session)
+ dreq = Rack::MockRequest.new(defer)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
+ res2["Set-Cookie"].should.equal nil
+ res2.body.should.equal '{"counter"=>3}'
+
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
+ res3["Set-Cookie"][session_match].should.equal session
+ res3.body.should.equal '{"counter"=>4}'
+ end
+
+ specify "deep hashes are correctly updated" do
+ store = nil
+ hash_check = proc do |env|
+ session = env['rack.session']
+ unless session.include? 'test'
+ session.update :a => :b, :c => { :d => :e },
+ :f => { :g => { :h => :i} }, 'test' => true
+ else
+ session[:f][:g][:h] = :j
+ end
+ [200, {}, session.inspect]
+ end
+ pool = Rack::Session::Memcache.new(hash_check)
+ req = Rack::MockRequest.new(pool)
+
+ res0 = req.get("/")
+ session_id = (cookie = res0["Set-Cookie"])[session_match, 1]
+ ses0 = pool.pool.get(session_id, true)
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ ses1 = pool.pool.get(session_id, true)
+
+ ses1.should.not.equal ses0
+ end
+
+ # anyone know how to do this better?
+ specify "multithread: should cleanly merge sessions" do
+ next unless $DEBUG
+ warn 'Running multithread test for Session::Memcache'
+ pool = Rack::Session::Memcache.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+
+ res = req.get('/')
+ res.body.should.equal '{"counter"=>1}'
+ cookie = res["Set-Cookie"]
+ session_id = cookie[session_match, 1]
+
+ delta_incrementor = lambda do |env|
+ # emulate disconjoinment of threading
+ env['rack.session'] = env['rack.session'].dup
+ Thread.stop
+ env['rack.session'][(Time.now.usec*rand).to_i] = true
+ incrementor.call(env)
+ end
+ tses = Rack::Utils::Context.new pool, delta_incrementor
+ treq = Rack::MockRequest.new(tses)
+ tnum = rand(7).to_i+5
+ r = Array.new(tnum) do
+ Thread.new(treq) do |run|
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
+ end
+ end.reverse.map{|t| t.run.join.value }
+ r.each do |request|
+ request['Set-Cookie'].should.equal cookie
+ request.body.should.include '"counter"=>2'
+ end
+
+ session = pool.pool.get(session_id)
+ session.size.should.be tnum+1 # counter
+ session['counter'].should.be 2 # meeeh
+
+ tnum = rand(7).to_i+5
+ r = Array.new(tnum) do |i|
+ delta_time = proc do |env|
+ env['rack.session'][i] = Time.now
+ Thread.stop
+ env['rack.session'] = env['rack.session'].dup
+ env['rack.session'][i] -= Time.now
+ incrementor.call(env)
+ end
+ app = Rack::Utils::Context.new pool, time_delta
+ req = Rack::MockRequest.new app
+ Thread.new(req) do |run|
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
+ end
+ end.reverse.map{|t| t.run.join.value }
+ r.each do |request|
+ request['Set-Cookie'].should.equal cookie
+ request.body.should.include '"counter"=>3'
+ end
+
+ session = pool.pool.get(session_id)
+ session.size.should.be tnum+1
+ session['counter'].should.be 3
+
+ drop_counter = proc do |env|
+ env['rack.session'].delete 'counter'
+ env['rack.session']['foo'] = 'bar'
+ [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
+ end
+ tses = Rack::Utils::Context.new pool, drop_counter
+ treq = Rack::MockRequest.new(tses)
+ tnum = rand(7).to_i+5
+ r = Array.new(tnum) do
+ Thread.new(treq) do |run|
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
+ end
+ end.reverse.map{|t| t.run.join.value }
+ r.each do |request|
+ request['Set-Cookie'].should.equal cookie
+ request.body.should.include '"foo"=>"bar"'
+ end
+
+ session = pool.pool.get(session_id)
+ session.size.should.be r.size+1
+ session['counter'].should.be.nil?
+ session['foo'].should.equal 'bar'
+ end
+ end
+rescue RuntimeError
+ $stderr.puts "Skipping Rack::Session::Memcache tests. Start memcached and try again."
+rescue LoadError
+ $stderr.puts "Skipping Rack::Session::Memcache tests (Memcache is required). `gem install memcache-client` and try again."
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_session_pool.rb b/vendor/gems/rack-1.1.0/test/spec_rack_session_pool.rb
new file mode 100644
index 00000000..6be382ec
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_session_pool.rb
@@ -0,0 +1,172 @@
+require 'test/spec'
+
+require 'rack/session/pool'
+require 'rack/mock'
+require 'rack/response'
+require 'thread'
+
+context "Rack::Session::Pool" do
+ session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key]
+ session_match = /#{session_key}=[0-9a-fA-F]+;/
+ incrementor = lambda do |env|
+ env["rack.session"]["counter"] ||= 0
+ env["rack.session"]["counter"] += 1
+ Rack::Response.new(env["rack.session"].inspect).to_a
+ end
+ drop_session = proc do |env|
+ env['rack.session.options'][:drop] = true
+ incrementor.call(env)
+ end
+ renew_session = proc do |env|
+ env['rack.session.options'][:renew] = true
+ incrementor.call(env)
+ end
+ defer_session = proc do |env|
+ env['rack.session.options'][:defer] = true
+ incrementor.call(env)
+ end
+
+ specify "creates a new cookie" do
+ pool = Rack::Session::Pool.new(incrementor)
+ res = Rack::MockRequest.new(pool).get("/")
+ res["Set-Cookie"].should.match session_match
+ res.body.should.equal '{"counter"=>1}'
+ end
+
+ specify "determines session from a cookie" do
+ pool = Rack::Session::Pool.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ cookie = req.get("/")["Set-Cookie"]
+ req.get("/", "HTTP_COOKIE" => cookie).
+ body.should.equal '{"counter"=>2}'
+ req.get("/", "HTTP_COOKIE" => cookie).
+ body.should.equal '{"counter"=>3}'
+ end
+
+ specify "survives nonexistant cookies" do
+ pool = Rack::Session::Pool.new(incrementor)
+ res = Rack::MockRequest.new(pool).
+ get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel")
+ res.body.should.equal '{"counter"=>1}'
+ end
+
+ specify "deletes cookies with :drop option" do
+ pool = Rack::Session::Pool.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ drop = Rack::Utils::Context.new(pool, drop_session)
+ dreq = Rack::MockRequest.new(drop)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+ pool.pool.size.should.be 1
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+ pool.pool.size.should.be 1
+
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
+ res2["Set-Cookie"].should.equal nil
+ res2.body.should.equal '{"counter"=>3}'
+ pool.pool.size.should.be 0
+
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
+ res3["Set-Cookie"][session_match].should.not.equal session
+ res3.body.should.equal '{"counter"=>1}'
+ pool.pool.size.should.be 1
+ end
+
+ specify "provides new session id with :renew option" do
+ pool = Rack::Session::Pool.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ renew = Rack::Utils::Context.new(pool, renew_session)
+ rreq = Rack::MockRequest.new(renew)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+ pool.pool.size.should.be 1
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+ pool.pool.size.should.be 1
+
+ res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
+ new_cookie = res2["Set-Cookie"]
+ new_session = new_cookie[session_match]
+ new_session.should.not.equal session
+ res2.body.should.equal '{"counter"=>3}'
+ pool.pool.size.should.be 1
+
+ res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
+ res3["Set-Cookie"][session_match].should.equal new_session
+ res3.body.should.equal '{"counter"=>4}'
+ pool.pool.size.should.be 1
+ end
+
+ specify "omits cookie with :defer option" do
+ pool = Rack::Session::Pool.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+ defer = Rack::Utils::Context.new(pool, defer_session)
+ dreq = Rack::MockRequest.new(defer)
+
+ res0 = req.get("/")
+ session = (cookie = res0["Set-Cookie"])[session_match]
+ res0.body.should.equal '{"counter"=>1}'
+ pool.pool.size.should.be 1
+
+ res1 = req.get("/", "HTTP_COOKIE" => cookie)
+ res1["Set-Cookie"][session_match].should.equal session
+ res1.body.should.equal '{"counter"=>2}'
+ pool.pool.size.should.be 1
+
+ res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
+ res2["Set-Cookie"].should.equal nil
+ res2.body.should.equal '{"counter"=>3}'
+ pool.pool.size.should.be 1
+
+ res3 = req.get("/", "HTTP_COOKIE" => cookie)
+ res3["Set-Cookie"][session_match].should.equal session
+ res3.body.should.equal '{"counter"=>4}'
+ pool.pool.size.should.be 1
+ end
+
+ # anyone know how to do this better?
+ specify "multithread: should merge sessions" do
+ next unless $DEBUG
+ warn 'Running multithread tests for Session::Pool'
+ pool = Rack::Session::Pool.new(incrementor)
+ req = Rack::MockRequest.new(pool)
+
+ res = req.get('/')
+ res.body.should.equal '{"counter"=>1}'
+ cookie = res["Set-Cookie"]
+ sess_id = cookie[/#{pool.key}=([^,;]+)/,1]
+
+ delta_incrementor = lambda do |env|
+ # emulate disconjoinment of threading
+ env['rack.session'] = env['rack.session'].dup
+ Thread.stop
+ env['rack.session'][(Time.now.usec*rand).to_i] = true
+ incrementor.call(env)
+ end
+ tses = Rack::Utils::Context.new pool, delta_incrementor
+ treq = Rack::MockRequest.new(tses)
+ tnum = rand(7).to_i+5
+ r = Array.new(tnum) do
+ Thread.new(treq) do |run|
+ run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
+ end
+ end.reverse.map{|t| t.run.join.value }
+ r.each do |res|
+ res['Set-Cookie'].should.equal cookie
+ res.body.should.include '"counter"=>2'
+ end
+
+ session = pool.pool[sess_id]
+ session.size.should.be tnum+1 # counter
+ session['counter'].should.be 2 # meeeh
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_showexceptions.rb b/vendor/gems/rack-1.1.0/test/spec_rack_showexceptions.rb
new file mode 100644
index 00000000..bdbc1201
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_showexceptions.rb
@@ -0,0 +1,21 @@
+require 'test/spec'
+
+require 'rack/showexceptions'
+require 'rack/mock'
+
+context "Rack::ShowExceptions" do
+ specify "catches exceptions" do
+ res = nil
+ req = Rack::MockRequest.new(Rack::ShowExceptions.new(lambda { |env|
+ raise RuntimeError
+ }))
+ lambda {
+ res = req.get("/")
+ }.should.not.raise
+ res.should.be.a.server_error
+ res.status.should.equal 500
+
+ res.should =~ /RuntimeError/
+ res.should =~ /ShowExceptions/
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_showstatus.rb b/vendor/gems/rack-1.1.0/test/spec_rack_showstatus.rb
new file mode 100644
index 00000000..78700134
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_showstatus.rb
@@ -0,0 +1,72 @@
+require 'test/spec'
+
+require 'rack/showstatus'
+require 'rack/mock'
+
+context "Rack::ShowStatus" do
+ specify "should provide a default status message" do
+ req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+ }))
+
+ res = req.get("/", :lint => true)
+ res.should.be.not_found
+ res.should.be.not.empty
+
+ res["Content-Type"].should.equal("text/html")
+ res.should =~ /404/
+ res.should =~ /Not Found/
+ end
+
+ specify "should let the app provide additional information" do
+ req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
+ env["rack.showstatus.detail"] = "gone too meta."
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+ }))
+
+ res = req.get("/", :lint => true)
+ res.should.be.not_found
+ res.should.be.not.empty
+
+ res["Content-Type"].should.equal("text/html")
+ res.should =~ /404/
+ res.should =~ /Not Found/
+ res.should =~ /too meta/
+ end
+
+ specify "should not replace existing messages" do
+ req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
+ }))
+ res = req.get("/", :lint => true)
+ res.should.be.not_found
+
+ res.body.should == "foo!"
+ end
+
+ specify "should pass on original headers" do
+ headers = {"WWW-Authenticate" => "Basic blah"}
+
+ req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env| [401, headers, []] }))
+ res = req.get("/", :lint => true)
+
+ res["WWW-Authenticate"].should.equal("Basic blah")
+ end
+
+ specify "should replace existing messages if there is detail" do
+ req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
+ env["rack.showstatus.detail"] = "gone too meta."
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
+ }))
+
+ res = req.get("/", :lint => true)
+ res.should.be.not_found
+ res.should.be.not.empty
+
+ res["Content-Type"].should.equal("text/html")
+ res["Content-Length"].should.not.equal("4")
+ res.should =~ /404/
+ res.should =~ /too meta/
+ res.body.should.not =~ /foo/
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_static.rb b/vendor/gems/rack-1.1.0/test/spec_rack_static.rb
new file mode 100644
index 00000000..19d2ecb7
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_static.rb
@@ -0,0 +1,37 @@
+require 'test/spec'
+
+require 'rack/static'
+require 'rack/mock'
+
+class DummyApp
+ def call(env)
+ [200, {}, ["Hello World"]]
+ end
+end
+
+context "Rack::Static" do
+ root = File.expand_path(File.dirname(__FILE__))
+ OPTIONS = {:urls => ["/cgi"], :root => root}
+
+ setup do
+ @request = Rack::MockRequest.new(Rack::Static.new(DummyApp.new, OPTIONS))
+ end
+
+ specify "serves files" do
+ res = @request.get("/cgi/test")
+ res.should.be.ok
+ res.body.should =~ /ruby/
+ end
+
+ specify "404s if url root is known but it can't find the file" do
+ res = @request.get("/cgi/foo")
+ res.should.be.not_found
+ end
+
+ specify "calls down the chain if url root is not known" do
+ res = @request.get("/something/else")
+ res.should.be.ok
+ res.body.should == "Hello World"
+ end
+
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_thin.rb b/vendor/gems/rack-1.1.0/test/spec_rack_thin.rb
new file mode 100644
index 00000000..324f6498
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_thin.rb
@@ -0,0 +1,91 @@
+require 'test/spec'
+
+begin
+require 'rack/handler/thin'
+require 'testrequest'
+require 'timeout'
+
+context "Rack::Handler::Thin" do
+ include TestRequest::Helpers
+
+ setup do
+ @app = Rack::Lint.new(TestRequest.new)
+ @server = nil
+ Thin::Logging.silent = true
+ @thread = Thread.new do
+ Rack::Handler::Thin.run(@app, :Host => @host='0.0.0.0', :Port => @port=9204) do |server|
+ @server = server
+ end
+ end
+ Thread.pass until @server && @server.running?
+ end
+
+ specify "should respond" do
+ lambda {
+ GET("/")
+ }.should.not.raise
+ end
+
+ specify "should be a Thin" do
+ GET("/")
+ status.should.be 200
+ response["SERVER_SOFTWARE"].should =~ /thin/
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
+ response["SERVER_PORT"].should.equal "9204"
+ response["SERVER_NAME"].should.equal "0.0.0.0"
+ end
+
+ specify "should have rack headers" do
+ GET("/")
+ response["rack.version"].should.equal [0,3]
+ response["rack.multithread"].should.be false
+ response["rack.multiprocess"].should.be false
+ response["rack.run_once"].should.be false
+ end
+
+ specify "should have CGI headers on GET" do
+ GET("/")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.be.equal "/"
+ response["QUERY_STRING"].should.equal ""
+ response["test.postdata"].should.equal ""
+
+ GET("/test/foo?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["REQUEST_PATH"].should.equal "/test/foo"
+ response["PATH_INFO"].should.equal "/test/foo"
+ response["QUERY_STRING"].should.equal "quux=1"
+ end
+
+ specify "should have CGI headers on POST" do
+ POST("/", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+ status.should.equal 200
+ response["REQUEST_METHOD"].should.equal "POST"
+ response["REQUEST_PATH"].should.equal "/"
+ response["QUERY_STRING"].should.equal ""
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
+ response["test.postdata"].should.equal "rack-form-data=23"
+ end
+
+ specify "should support HTTP auth" do
+ GET("/test", {:user => "ruth", :passwd => "secret"})
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
+ end
+
+ specify "should set status" do
+ GET("/test?secret")
+ status.should.equal 403
+ response["rack.url_scheme"].should.equal "http"
+ end
+
+ teardown do
+ @server.stop!
+ @thread.kill
+ end
+end
+
+rescue LoadError
+ $stderr.puts "Skipping Rack::Handler::Thin tests (Thin is required). `gem install thin` and try again."
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_urlmap.rb b/vendor/gems/rack-1.1.0/test/spec_rack_urlmap.rb
new file mode 100644
index 00000000..3d8fe605
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_urlmap.rb
@@ -0,0 +1,215 @@
+require 'test/spec'
+
+require 'rack/urlmap'
+require 'rack/mock'
+
+context "Rack::URLMap" do
+ specify "dispatches paths correctly" do
+ app = lambda { |env|
+ [200, {
+ 'X-ScriptName' => env['SCRIPT_NAME'],
+ 'X-PathInfo' => env['PATH_INFO'],
+ 'Content-Type' => 'text/plain'
+ }, [""]]
+ }
+ map = Rack::URLMap.new({
+ 'http://foo.org/bar' => app,
+ '/foo' => app,
+ '/foo/bar' => app
+ })
+
+ res = Rack::MockRequest.new(map).get("/")
+ res.should.be.not_found
+
+ res = Rack::MockRequest.new(map).get("/qux")
+ res.should.be.not_found
+
+ res = Rack::MockRequest.new(map).get("/foo")
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/foo"
+ res["X-PathInfo"].should.equal ""
+
+ res = Rack::MockRequest.new(map).get("/foo/")
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/foo"
+ res["X-PathInfo"].should.equal "/"
+
+ res = Rack::MockRequest.new(map).get("/foo/bar")
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/foo/bar"
+ res["X-PathInfo"].should.equal ""
+
+ res = Rack::MockRequest.new(map).get("/foo/bar/")
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/foo/bar"
+ res["X-PathInfo"].should.equal "/"
+
+ res = Rack::MockRequest.new(map).get("/foo///bar//quux")
+ res.status.should.equal 200
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/foo/bar"
+ res["X-PathInfo"].should.equal "//quux"
+
+ res = Rack::MockRequest.new(map).get("/foo/quux", "SCRIPT_NAME" => "/bleh")
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/bleh/foo"
+ res["X-PathInfo"].should.equal "/quux"
+
+ res = Rack::MockRequest.new(map).get("/bar", 'HTTP_HOST' => 'foo.org')
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/bar"
+ res["X-PathInfo"].should.be.empty
+
+ res = Rack::MockRequest.new(map).get("/bar/", 'HTTP_HOST' => 'foo.org')
+ res.should.be.ok
+ res["X-ScriptName"].should.equal "/bar"
+ res["X-PathInfo"].should.equal '/'
+ end
+
+
+ specify "dispatches hosts correctly" do
+ map = Rack::URLMap.new("http://foo.org/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "foo.org",
+ "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+ }, [""]]},
+ "http://subdomain.foo.org/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "subdomain.foo.org",
+ "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+ }, [""]]},
+ "http://bar.org/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "bar.org",
+ "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+ }, [""]]},
+ "/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "default.org",
+ "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+ }, [""]]}
+ )
+
+ res = Rack::MockRequest.new(map).get("/")
+ res.should.be.ok
+ res["X-Position"].should.equal "default.org"
+
+ res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "bar.org")
+ res.should.be.ok
+ res["X-Position"].should.equal "bar.org"
+
+ res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "foo.org")
+ res.should.be.ok
+ res["X-Position"].should.equal "foo.org"
+
+ res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "subdomain.foo.org", "SERVER_NAME" => "foo.org")
+ res.should.be.ok
+ res["X-Position"].should.equal "subdomain.foo.org"
+
+ res = Rack::MockRequest.new(map).get("http://foo.org/")
+ res.should.be.ok
+ res["X-Position"].should.equal "default.org"
+
+ res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "example.org")
+ res.should.be.ok
+ res["X-Position"].should.equal "default.org"
+
+ res = Rack::MockRequest.new(map).get("/",
+ "HTTP_HOST" => "example.org:9292",
+ "SERVER_PORT" => "9292")
+ res.should.be.ok
+ res["X-Position"].should.equal "default.org"
+ end
+
+ specify "should be nestable" do
+ map = Rack::URLMap.new("/foo" =>
+ Rack::URLMap.new("/bar" =>
+ Rack::URLMap.new("/quux" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "/foo/bar/quux",
+ "X-PathInfo" => env["PATH_INFO"],
+ "X-ScriptName" => env["SCRIPT_NAME"],
+ }, [""]]}
+ )))
+
+ res = Rack::MockRequest.new(map).get("/foo/bar")
+ res.should.be.not_found
+
+ res = Rack::MockRequest.new(map).get("/foo/bar/quux")
+ res.should.be.ok
+ res["X-Position"].should.equal "/foo/bar/quux"
+ res["X-PathInfo"].should.equal ""
+ res["X-ScriptName"].should.equal "/foo/bar/quux"
+ end
+
+ specify "should route root apps correctly" do
+ map = Rack::URLMap.new("/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "root",
+ "X-PathInfo" => env["PATH_INFO"],
+ "X-ScriptName" => env["SCRIPT_NAME"]
+ }, [""]]},
+ "/foo" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "foo",
+ "X-PathInfo" => env["PATH_INFO"],
+ "X-ScriptName" => env["SCRIPT_NAME"]
+ }, [""]]}
+ )
+
+ res = Rack::MockRequest.new(map).get("/foo/bar")
+ res.should.be.ok
+ res["X-Position"].should.equal "foo"
+ res["X-PathInfo"].should.equal "/bar"
+ res["X-ScriptName"].should.equal "/foo"
+
+ res = Rack::MockRequest.new(map).get("/foo")
+ res.should.be.ok
+ res["X-Position"].should.equal "foo"
+ res["X-PathInfo"].should.equal ""
+ res["X-ScriptName"].should.equal "/foo"
+
+ res = Rack::MockRequest.new(map).get("/bar")
+ res.should.be.ok
+ res["X-Position"].should.equal "root"
+ res["X-PathInfo"].should.equal "/bar"
+ res["X-ScriptName"].should.equal ""
+
+ res = Rack::MockRequest.new(map).get("")
+ res.should.be.ok
+ res["X-Position"].should.equal "root"
+ res["X-PathInfo"].should.equal "/"
+ res["X-ScriptName"].should.equal ""
+ end
+
+ specify "should not squeeze slashes" do
+ map = Rack::URLMap.new("/" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "root",
+ "X-PathInfo" => env["PATH_INFO"],
+ "X-ScriptName" => env["SCRIPT_NAME"]
+ }, [""]]},
+ "/foo" => lambda { |env|
+ [200,
+ { "Content-Type" => "text/plain",
+ "X-Position" => "foo",
+ "X-PathInfo" => env["PATH_INFO"],
+ "X-ScriptName" => env["SCRIPT_NAME"]
+ }, [""]]}
+ )
+
+ res = Rack::MockRequest.new(map).get("/http://example.org/bar")
+ res.should.be.ok
+ res["X-Position"].should.equal "root"
+ res["X-PathInfo"].should.equal "/http://example.org/bar"
+ res["X-ScriptName"].should.equal ""
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_utils.rb b/vendor/gems/rack-1.1.0/test/spec_rack_utils.rb
new file mode 100644
index 00000000..269a52bd
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_utils.rb
@@ -0,0 +1,552 @@
+require 'test/spec'
+
+require 'rack/utils'
+require 'rack/lint'
+require 'rack/mock'
+
+context "Rack::Utils" do
+ specify "should escape correctly" do
+ Rack::Utils.escape("fobar").should.equal "fo%3Co%3Ebar"
+ Rack::Utils.escape("a space").should.equal "a+space"
+ Rack::Utils.escape("q1!2\"'w$5&7/z8)?\\").
+ should.equal "q1%212%22%27w%245%267%2Fz8%29%3F%5C"
+ end
+
+ specify "should escape correctly for multibyte characters" do
+ matz_name = "\xE3\x81\xBE\xE3\x81\xA4\xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsumoto
+ matz_name.force_encoding("UTF-8") if matz_name.respond_to? :force_encoding
+ Rack::Utils.escape(matz_name).should.equal '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8'
+ matz_name_sep = "\xE3\x81\xBE\xE3\x81\xA4 \xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsu moto
+ matz_name_sep.force_encoding("UTF-8") if matz_name_sep.respond_to? :force_encoding
+ Rack::Utils.escape(matz_name_sep).should.equal '%E3%81%BE%E3%81%A4+%E3%82%82%E3%81%A8'
+ end
+
+ specify "should unescape correctly" do
+ Rack::Utils.unescape("fo%3Co%3Ebar").should.equal "fobar"
+ Rack::Utils.unescape("a+space").should.equal "a space"
+ Rack::Utils.unescape("a%20space").should.equal "a space"
+ Rack::Utils.unescape("q1%212%22%27w%245%267%2Fz8%29%3F%5C").
+ should.equal "q1!2\"'w$5&7/z8)?\\"
+ end
+
+ specify "should parse query strings correctly" do
+ Rack::Utils.parse_query("foo=bar").
+ should.equal "foo" => "bar"
+ Rack::Utils.parse_query("foo=\"bar\"").
+ should.equal "foo" => "bar"
+ Rack::Utils.parse_query("foo=bar&foo=quux").
+ should.equal "foo" => ["bar", "quux"]
+ Rack::Utils.parse_query("foo=1&bar=2").
+ should.equal "foo" => "1", "bar" => "2"
+ Rack::Utils.parse_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F").
+ should.equal "my weird field" => "q1!2\"'w$5&7/z8)?"
+ Rack::Utils.parse_query("foo%3Dbaz=bar").should.equal "foo=baz" => "bar"
+ end
+
+ specify "should parse nested query strings correctly" do
+ Rack::Utils.parse_nested_query("foo").
+ should.equal "foo" => nil
+ Rack::Utils.parse_nested_query("foo=").
+ should.equal "foo" => ""
+ Rack::Utils.parse_nested_query("foo=bar").
+ should.equal "foo" => "bar"
+ Rack::Utils.parse_nested_query("foo=\"bar\"").
+ should.equal "foo" => "bar"
+
+ Rack::Utils.parse_nested_query("foo=bar&foo=quux").
+ should.equal "foo" => "quux"
+ Rack::Utils.parse_nested_query("foo&foo=").
+ should.equal "foo" => ""
+ Rack::Utils.parse_nested_query("foo=1&bar=2").
+ should.equal "foo" => "1", "bar" => "2"
+ Rack::Utils.parse_nested_query("&foo=1&&bar=2").
+ should.equal "foo" => "1", "bar" => "2"
+ Rack::Utils.parse_nested_query("foo&bar=").
+ should.equal "foo" => nil, "bar" => ""
+ Rack::Utils.parse_nested_query("foo=bar&baz=").
+ should.equal "foo" => "bar", "baz" => ""
+ Rack::Utils.parse_nested_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F").
+ should.equal "my weird field" => "q1!2\"'w$5&7/z8)?"
+
+ Rack::Utils.parse_nested_query("foo[]").
+ should.equal "foo" => [nil]
+ Rack::Utils.parse_nested_query("foo[]=").
+ should.equal "foo" => [""]
+ Rack::Utils.parse_nested_query("foo[]=bar").
+ should.equal "foo" => ["bar"]
+
+ Rack::Utils.parse_nested_query("foo[]=1&foo[]=2").
+ should.equal "foo" => ["1", "2"]
+ Rack::Utils.parse_nested_query("foo=bar&baz[]=1&baz[]=2&baz[]=3").
+ should.equal "foo" => "bar", "baz" => ["1", "2", "3"]
+ Rack::Utils.parse_nested_query("foo[]=bar&baz[]=1&baz[]=2&baz[]=3").
+ should.equal "foo" => ["bar"], "baz" => ["1", "2", "3"]
+
+ Rack::Utils.parse_nested_query("x[y][z]=1").
+ should.equal "x" => {"y" => {"z" => "1"}}
+ Rack::Utils.parse_nested_query("x[y][z][]=1").
+ should.equal "x" => {"y" => {"z" => ["1"]}}
+ Rack::Utils.parse_nested_query("x[y][z]=1&x[y][z]=2").
+ should.equal "x" => {"y" => {"z" => "2"}}
+ Rack::Utils.parse_nested_query("x[y][z][]=1&x[y][z][]=2").
+ should.equal "x" => {"y" => {"z" => ["1", "2"]}}
+
+ Rack::Utils.parse_nested_query("x[y][][z]=1").
+ should.equal "x" => {"y" => [{"z" => "1"}]}
+ Rack::Utils.parse_nested_query("x[y][][z][]=1").
+ should.equal "x" => {"y" => [{"z" => ["1"]}]}
+ Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=2").
+ should.equal "x" => {"y" => [{"z" => "1", "w" => "2"}]}
+
+ Rack::Utils.parse_nested_query("x[y][][v][w]=1").
+ should.equal "x" => {"y" => [{"v" => {"w" => "1"}}]}
+ Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][v][w]=2").
+ should.equal "x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}
+
+ Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][z]=2").
+ should.equal "x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}
+ Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3").
+ should.equal "x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}
+
+ lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y]z=2") }.
+ should.raise(TypeError).
+ message.should.equal "expected Hash (got String) for param `y'"
+
+ lambda { Rack::Utils.parse_nested_query("x[y]=1&x[]=1") }.
+ should.raise(TypeError).
+ message.should.equal "expected Array (got Hash) for param `x'"
+
+ lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y][][w]=2") }.
+ should.raise(TypeError).
+ message.should.equal "expected Array (got String) for param `y'"
+ end
+
+ specify "should build query strings correctly" do
+ Rack::Utils.build_query("foo" => "bar").should.equal "foo=bar"
+ Rack::Utils.build_query("foo" => ["bar", "quux"]).
+ should.equal "foo=bar&foo=quux"
+ Rack::Utils.build_query("foo" => "1", "bar" => "2").
+ should.equal "foo=1&bar=2"
+ Rack::Utils.build_query("my weird field" => "q1!2\"'w$5&7/z8)?").
+ should.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F"
+ end
+
+ specify "should build nested query strings correctly" do
+ Rack::Utils.build_nested_query("foo" => nil).should.equal "foo"
+ Rack::Utils.build_nested_query("foo" => "").should.equal "foo="
+ Rack::Utils.build_nested_query("foo" => "bar").should.equal "foo=bar"
+
+ Rack::Utils.build_nested_query("foo" => "1", "bar" => "2").
+ should.equal "foo=1&bar=2"
+ Rack::Utils.build_nested_query("my weird field" => "q1!2\"'w$5&7/z8)?").
+ should.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F"
+
+ Rack::Utils.build_nested_query("foo" => [nil]).
+ should.equal "foo[]"
+ Rack::Utils.build_nested_query("foo" => [""]).
+ should.equal "foo[]="
+ Rack::Utils.build_nested_query("foo" => ["bar"]).
+ should.equal "foo[]=bar"
+
+ # The ordering of the output query string is unpredictable with 1.8's
+ # unordered hash. Test that build_nested_query performs the inverse
+ # function of parse_nested_query.
+ [{"foo" => nil, "bar" => ""},
+ {"foo" => "bar", "baz" => ""},
+ {"foo" => ["1", "2"]},
+ {"foo" => "bar", "baz" => ["1", "2", "3"]},
+ {"foo" => ["bar"], "baz" => ["1", "2", "3"]},
+ {"foo" => ["1", "2"]},
+ {"foo" => "bar", "baz" => ["1", "2", "3"]},
+ {"x" => {"y" => {"z" => "1"}}},
+ {"x" => {"y" => {"z" => ["1"]}}},
+ {"x" => {"y" => {"z" => ["1", "2"]}}},
+ {"x" => {"y" => [{"z" => "1"}]}},
+ {"x" => {"y" => [{"z" => ["1"]}]}},
+ {"x" => {"y" => [{"z" => "1", "w" => "2"}]}},
+ {"x" => {"y" => [{"v" => {"w" => "1"}}]}},
+ {"x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}},
+ {"x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}},
+ {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}}
+ ].each { |params|
+ qs = Rack::Utils.build_nested_query(params)
+ Rack::Utils.parse_nested_query(qs).should.equal params
+ }
+
+ lambda { Rack::Utils.build_nested_query("foo=bar") }.
+ should.raise(ArgumentError).
+ message.should.equal "value must be a Hash"
+ end
+
+ specify "should figure out which encodings are acceptable" do
+ helper = lambda do |a, b|
+ request = Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => a))
+ Rack::Utils.select_best_encoding(a, b)
+ end
+
+ helper.call(%w(), [["x", 1]]).should.equal(nil)
+ helper.call(%w(identity), [["identity", 0.0]]).should.equal(nil)
+ helper.call(%w(identity), [["*", 0.0]]).should.equal(nil)
+
+ helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("identity")
+
+ helper.call(%w(compress gzip identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("compress")
+ helper.call(%w(compress gzip identity), [["compress", 0.5], ["gzip", 1.0]]).should.equal("gzip")
+
+ helper.call(%w(foo bar identity), []).should.equal("identity")
+ helper.call(%w(foo bar identity), [["*", 1.0]]).should.equal("foo")
+ helper.call(%w(foo bar identity), [["*", 1.0], ["foo", 0.9]]).should.equal("bar")
+
+ helper.call(%w(foo bar identity), [["foo", 0], ["bar", 0]]).should.equal("identity")
+ helper.call(%w(foo bar baz identity), [["*", 0], ["identity", 0.1]]).should.equal("identity")
+ end
+
+ specify "should return the bytesize of String" do
+ Rack::Utils.bytesize("FOO\xE2\x82\xAC").should.equal 6
+ end
+
+ specify "should return status code for integer" do
+ Rack::Utils.status_code(200).should.equal 200
+ end
+
+ specify "should return status code for string" do
+ Rack::Utils.status_code("200").should.equal 200
+ end
+
+ specify "should return status code for symbol" do
+ Rack::Utils.status_code(:ok).should.equal 200
+ end
+end
+
+context "Rack::Utils::HeaderHash" do
+ specify "should retain header case" do
+ h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
+ h['ETag'] = 'Boo!'
+ h.to_hash.should.equal "Content-MD5" => "d5ff4e2a0 ...", "ETag" => 'Boo!'
+ end
+
+ specify "should check existence of keys case insensitively" do
+ h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
+ h.should.include 'content-md5'
+ h.should.not.include 'ETag'
+ end
+
+ specify "should merge case-insensitively" do
+ h = Rack::Utils::HeaderHash.new("ETag" => 'HELLO', "content-length" => '123')
+ merged = h.merge("Etag" => 'WORLD', 'Content-Length' => '321', "Foo" => 'BAR')
+ merged.should.equal "Etag"=>'WORLD', "Content-Length"=>'321', "Foo"=>'BAR'
+ end
+
+ specify "should overwrite case insensitively and assume the new key's case" do
+ h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
+ h["foo-bar"] = "bizzle"
+ h["FOO-BAR"].should.equal "bizzle"
+ h.length.should.equal 1
+ h.to_hash.should.equal "foo-bar" => "bizzle"
+ end
+
+ specify "should be converted to real Hash" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.to_hash.should.be.instance_of Hash
+ end
+
+ specify "should convert Array values to Strings when converting to Hash" do
+ h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
+ h.to_hash.should.equal({ "foo" => "bar\nbaz" })
+ end
+
+ specify "should replace hashes correctly" do
+ h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
+ j = {"foo" => "bar"}
+ h.replace(j)
+ h["foo"].should.equal "bar"
+ end
+
+ specify "should be able to delete the given key case-sensitively" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("foo")
+ h["foo"].should.be.nil
+ h["FOO"].should.be.nil
+ end
+
+ specify "should be able to delete the given key case-insensitively" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("FOO")
+ h["foo"].should.be.nil
+ h["FOO"].should.be.nil
+ end
+
+ specify "should return the deleted value when #delete is called on an existing key" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("Foo").should.equal("bar")
+ end
+
+ specify "should return nil when #delete is called on a non-existant key" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("Hello").should.be.nil
+ end
+
+ specify "should avoid unnecessary object creation if possible" do
+ a = Rack::Utils::HeaderHash.new("foo" => "bar")
+ b = Rack::Utils::HeaderHash.new(a)
+ b.object_id.should.equal(a.object_id)
+ b.should.equal(a)
+ end
+
+ specify "should convert Array values to Strings when responding to #each" do
+ h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
+ h.each do |k,v|
+ k.should.equal("foo")
+ v.should.equal("bar\nbaz")
+ end
+ end
+
+end
+
+context "Rack::Utils::Context" do
+ class ContextTest
+ attr_reader :app
+ def initialize app; @app=app; end
+ def call env; context env; end
+ def context env, app=@app; app.call(env); end
+ end
+ test_target1 = proc{|e| e.to_s+' world' }
+ test_target2 = proc{|e| e.to_i+2 }
+ test_target3 = proc{|e| nil }
+ test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain', 'Content-Length'=>'0'},['']] }
+ test_app = ContextTest.new test_target4
+
+ specify "should set context correctly" do
+ test_app.app.should.equal test_target4
+ c1 = Rack::Utils::Context.new(test_app, test_target1)
+ c1.for.should.equal test_app
+ c1.app.should.equal test_target1
+ c2 = Rack::Utils::Context.new(test_app, test_target2)
+ c2.for.should.equal test_app
+ c2.app.should.equal test_target2
+ end
+
+ specify "should alter app on recontexting" do
+ c1 = Rack::Utils::Context.new(test_app, test_target1)
+ c2 = c1.recontext(test_target2)
+ c2.for.should.equal test_app
+ c2.app.should.equal test_target2
+ c3 = c2.recontext(test_target3)
+ c3.for.should.equal test_app
+ c3.app.should.equal test_target3
+ end
+
+ specify "should run different apps" do
+ c1 = Rack::Utils::Context.new test_app, test_target1
+ c2 = c1.recontext test_target2
+ c3 = c2.recontext test_target3
+ c4 = c3.recontext test_target4
+ a4 = Rack::Lint.new c4
+ a5 = Rack::Lint.new test_app
+ r1 = c1.call('hello')
+ r1.should.equal 'hello world'
+ r2 = c2.call(2)
+ r2.should.equal 4
+ r3 = c3.call(:misc_symbol)
+ r3.should.be.nil
+ r4 = Rack::MockRequest.new(a4).get('/')
+ r4.status.should.be 200
+ r5 = Rack::MockRequest.new(a5).get('/')
+ r5.status.should.be 200
+ r4.body.should.equal r5.body
+ end
+end
+
+context "Rack::Utils::Multipart" do
+ specify "should return nil if content type is not multipart" do
+ env = Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => 'application/x-www-form-urlencoded')
+ Rack::Utils::Multipart.parse_multipart(env).should.equal nil
+ end
+
+ specify "should parse multipart upload with text file" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"][:type].should.equal "text/plain"
+ params["files"][:filename].should.equal "file1.txt"
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n"
+ params["files"][:name].should.equal "files"
+ params["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "should parse multipart upload with nested parameters" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:nested))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["foo"]["submit-name"].should.equal "Larry"
+ params["foo"]["files"][:type].should.equal "text/plain"
+ params["foo"]["files"][:filename].should.equal "file1.txt"
+ params["foo"]["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"foo[files]\"; filename=\"file1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n"
+ params["foo"]["files"][:name].should.equal "foo[files]"
+ params["foo"]["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "should parse multipart upload with binary file" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:binary))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"][:type].should.equal "image/png"
+ params["files"][:filename].should.equal "rack-logo.png"
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"files\"; filename=\"rack-logo.png\"\r\n" +
+ "Content-Type: image/png\r\n"
+ params["files"][:name].should.equal "files"
+ params["files"][:tempfile].read.length.should.equal 26473
+ end
+
+ specify "should parse multipart upload with empty file" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:empty))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"][:type].should.equal "text/plain"
+ params["files"][:filename].should.equal "file1.txt"
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"files\"; filename=\"file1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n"
+ params["files"][:name].should.equal "files"
+ params["files"][:tempfile].read.should.equal ""
+ end
+
+ specify "should parse multipart upload with filename with semicolons" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:semicolon))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["files"][:type].should.equal "text/plain"
+ params["files"][:filename].should.equal "fi;le1.txt"
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"files\"; filename=\"fi;le1.txt\"\r\n" +
+ "Content-Type: text/plain\r\n"
+ params["files"][:name].should.equal "files"
+ params["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "should not include file params if no file was selected" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"].should.equal nil
+ params.keys.should.not.include "files"
+ end
+
+ specify "should parse IE multipart upload and clean up filename" do
+ env = Rack::MockRequest.env_for("/", multipart_fixture(:ie))
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["files"][:type].should.equal "text/plain"
+ params["files"][:filename].should.equal "file1.txt"
+ params["files"][:head].should.equal "Content-Disposition: form-data; " +
+ "name=\"files\"; " +
+ 'filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"' +
+ "\r\nContent-Type: text/plain\r\n"
+ params["files"][:name].should.equal "files"
+ params["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "rewinds input after parsing upload" do
+ options = multipart_fixture(:text)
+ input = options[:input]
+ env = Rack::MockRequest.env_for("/", options)
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"][:filename].should.equal "file1.txt"
+ input.read.length.should.equal 197
+ end
+
+ specify "builds multipart body" do
+ files = Rack::Utils::Multipart::UploadedFile.new(multipart_file("file1.txt"))
+ data = Rack::Utils::Multipart.build_multipart("submit-name" => "Larry", "files" => files)
+
+ options = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+ "CONTENT_LENGTH" => data.length.to_s,
+ :input => StringIO.new(data)
+ }
+ env = Rack::MockRequest.env_for("/", options)
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["submit-name"].should.equal "Larry"
+ params["files"][:filename].should.equal "file1.txt"
+ params["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "builds nested multipart body" do
+ files = Rack::Utils::Multipart::UploadedFile.new(multipart_file("file1.txt"))
+ data = Rack::Utils::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => files}])
+
+ options = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+ "CONTENT_LENGTH" => data.length.to_s,
+ :input => StringIO.new(data)
+ }
+ env = Rack::MockRequest.env_for("/", options)
+ params = Rack::Utils::Multipart.parse_multipart(env)
+ params["people"][0]["submit-name"].should.equal "Larry"
+ params["people"][0]["files"][:filename].should.equal "file1.txt"
+ params["people"][0]["files"][:tempfile].read.should.equal "contents"
+ end
+
+ specify "can parse fields that end at the end of the buffer" do
+ input = File.read(multipart_file("bad_robots"))
+
+ req = Rack::Request.new Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => "multipart/form-data, boundary=1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon",
+ "CONTENT_LENGTH" => input.size,
+ :input => input)
+
+ req.POST['file.path'].should.equal "/var/tmp/uploads/4/0001728414"
+ req.POST['addresses'].should.not.equal nil
+ end
+
+ specify "builds complete params with the chunk size of 16384 slicing exactly on boundary" do
+ data = File.open(multipart_file("fail_16384_nofile")) { |f| f.read }.gsub(/\n/, "\r\n")
+ options = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=----WebKitFormBoundaryWsY0GnpbI5U7ztzo",
+ "CONTENT_LENGTH" => data.length.to_s,
+ :input => StringIO.new(data)
+ }
+ env = Rack::MockRequest.env_for("/", options)
+ params = Rack::Utils::Multipart.parse_multipart(env)
+
+ params.should.not.equal nil
+ params.keys.should.include "AAAAAAAAAAAAAAAAAAA"
+ params["AAAAAAAAAAAAAAAAAAA"].keys.should.include "PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"].keys.should.include "new"
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"].keys.should.include "-2"
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"].keys.should.include "ba_unit_id"
+ params["AAAAAAAAAAAAAAAAAAA"]["PLAPLAPLA_MEMMEMMEMM_ATTRATTRER"]["new"]["-2"]["ba_unit_id"].should.equal "1017"
+ end
+
+ specify "should return nil if no UploadedFiles were used" do
+ data = Rack::Utils::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}])
+ data.should.equal nil
+ end
+
+ specify "should raise ArgumentError if params is not a Hash" do
+ lambda { Rack::Utils::Multipart.build_multipart("foo=bar") }.
+ should.raise(ArgumentError).
+ message.should.equal "value must be a Hash"
+ end
+
+ private
+ def multipart_fixture(name)
+ file = multipart_file(name)
+ data = File.open(file, 'rb') { |io| io.read }
+
+ type = "multipart/form-data; boundary=AaB03x"
+ length = data.respond_to?(:bytesize) ? data.bytesize : data.size
+
+ { "CONTENT_TYPE" => type,
+ "CONTENT_LENGTH" => length.to_s,
+ :input => StringIO.new(data) }
+ end
+
+ def multipart_file(name)
+ File.join(File.dirname(__FILE__), "multipart", name.to_s)
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rack_webrick.rb b/vendor/gems/rack-1.1.0/test/spec_rack_webrick.rb
new file mode 100644
index 00000000..599425c4
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rack_webrick.rb
@@ -0,0 +1,130 @@
+require 'test/spec'
+
+require 'rack/handler/webrick'
+require 'rack/lint'
+require 'rack/response'
+require 'testrequest'
+
+Thread.abort_on_exception = true
+
+context "Rack::Handler::WEBrick" do
+ include TestRequest::Helpers
+
+ setup do
+ @server = WEBrick::HTTPServer.new(:Host => @host='0.0.0.0',
+ :Port => @port=9202,
+ :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
+ :AccessLog => [])
+ @server.mount "/test", Rack::Handler::WEBrick,
+ Rack::Lint.new(TestRequest.new)
+ Thread.new { @server.start }
+ trap(:INT) { @server.shutdown }
+ end
+
+ specify "should respond" do
+ lambda {
+ GET("/test")
+ }.should.not.raise
+ end
+
+ specify "should be a WEBrick" do
+ GET("/test")
+ status.should.be 200
+ response["SERVER_SOFTWARE"].should =~ /WEBrick/
+ response["HTTP_VERSION"].should.equal "HTTP/1.1"
+ response["SERVER_PROTOCOL"].should.equal "HTTP/1.1"
+ response["SERVER_PORT"].should.equal "9202"
+ response["SERVER_NAME"].should.equal "0.0.0.0"
+ end
+
+ specify "should have rack headers" do
+ GET("/test")
+ response["rack.version"].should.equal [1,1]
+ response["rack.multithread"].should.be true
+ response["rack.multiprocess"].should.be false
+ response["rack.run_once"].should.be false
+ end
+
+ specify "should have CGI headers on GET" do
+ GET("/test")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.be.equal ""
+ response["QUERY_STRING"].should.equal ""
+ response["test.postdata"].should.equal ""
+
+ GET("/test/foo?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal "/foo"
+ response["QUERY_STRING"].should.equal "quux=1"
+
+ GET("/test/foo%25encoding?quux=1")
+ response["REQUEST_METHOD"].should.equal "GET"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["PATH_INFO"].should.equal "/foo%25encoding"
+ response["QUERY_STRING"].should.equal "quux=1"
+ end
+
+ specify "should have CGI headers on POST" do
+ POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+ status.should.equal 200
+ response["REQUEST_METHOD"].should.equal "POST"
+ response["SCRIPT_NAME"].should.equal "/test"
+ response["REQUEST_PATH"].should.equal "/"
+ response["QUERY_STRING"].should.equal ""
+ response["HTTP_X_TEST_HEADER"].should.equal "42"
+ response["test.postdata"].should.equal "rack-form-data=23"
+ end
+
+ specify "should support HTTP auth" do
+ GET("/test", {:user => "ruth", :passwd => "secret"})
+ response["HTTP_AUTHORIZATION"].should.equal "Basic cnV0aDpzZWNyZXQ="
+ end
+
+ specify "should set status" do
+ GET("/test?secret")
+ status.should.equal 403
+ response["rack.url_scheme"].should.equal "http"
+ end
+
+ specify "should correctly set cookies" do
+ @server.mount "/cookie-test", Rack::Handler::WEBrick,
+ Rack::Lint.new(lambda { |req|
+ res = Rack::Response.new
+ res.set_cookie "one", "1"
+ res.set_cookie "two", "2"
+ res.finish
+ })
+
+ Net::HTTP.start(@host, @port) { |http|
+ res = http.get("/cookie-test")
+ res.code.to_i.should.equal 200
+ res.get_fields("set-cookie").should.equal ["one=1", "two=2"]
+ }
+ end
+
+ specify "should provide a .run" do
+ block_ran = false
+ catch(:done) {
+ Rack::Handler::WEBrick.run(lambda {},
+ {:Port => 9210,
+ :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
+ :AccessLog => []}) { |server|
+ block_ran = true
+ server.should.be.kind_of WEBrick::HTTPServer
+ @s = server
+ throw :done
+ }
+ }
+ block_ran.should.be true
+ @s.shutdown
+ end
+
+ teardown do
+ @server.shutdown
+ end
+end
diff --git a/vendor/gems/rack-1.1.0/test/spec_rackup.rb b/vendor/gems/rack-1.1.0/test/spec_rackup.rb
new file mode 100644
index 00000000..d9926fda
--- /dev/null
+++ b/vendor/gems/rack-1.1.0/test/spec_rackup.rb
@@ -0,0 +1,154 @@
+require 'test/spec'
+require 'testrequest'
+require 'rack/server'
+require 'open3'
+
+begin
+require "mongrel"
+
+context "rackup" do
+ include TestRequest::Helpers
+
+ def run_rackup(*args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ flags = args.first
+ @host = options[:host] || "0.0.0.0"
+ @port = options[:port] || 9292
+
+ Dir.chdir("#{root}/test/rackup") do
+ @in, @rackup, @err = Open3.popen3("#{Gem.ruby} -S #{rackup} #{flags}")
+ end
+
+ return if options[:port] == false
+
+ # Wait until the server is available
+ begin
+ GET("/")
+ rescue
+ sleep 0.05
+ retry
+ end
+ end
+
+ def output
+ @rackup.read
+ end
+
+ after do
+ # This doesn't actually return a response, so we rescue
+ GET "/die" rescue nil
+
+ Dir["#{root}/**/*.pid"].each do |file|
+ File.delete(file)
+ end
+
+ File.delete("#{root}/log_output") if File.exist?("#{root}/log_output")
+ end
+
+ specify "rackup" do
+ run_rackup
+ response["PATH_INFO"].should.equal '/'
+ response["test.$DEBUG"].should.be false
+ response["test.$EVAL"].should.be nil
+ response["test.$VERBOSE"].should.be false
+ response["test.Ping"].should.be nil
+ response["SERVER_SOFTWARE"].should.not =~ /webrick/
+ end
+
+ specify "rackup --help" do
+ run_rackup "--help", :port => false
+ output.should.match /--port/
+ end
+
+ specify "rackup --port" do
+ run_rackup "--port 9000", :port => 9000
+ response["SERVER_PORT"].should.equal "9000"
+ end
+
+ specify "rackup --debug" do
+ run_rackup "--debug"
+ response["test.$DEBUG"].should.be true
+ end
+
+ specify "rackup --eval" do
+ run_rackup %{--eval "BUKKIT = 'BUKKIT'"}
+ response["test.$EVAL"].should.equal "BUKKIT"
+ end
+
+ specify "rackup --warn" do
+ run_rackup %{--warn}
+ response["test.$VERBOSE"].should.be true
+ end
+
+ specify "rackup --include" do
+ run_rackup %{--include /foo/bar}
+ response["test.$LOAD_PATH"].should.include "/foo/bar"
+ end
+
+ specify "rackup --require" do
+ run_rackup %{--require ping}
+ response["test.Ping"].should.equal "constant"
+ end
+
+ specify "rackup --server" do
+ run_rackup %{--server webrick}
+ response["SERVER_SOFTWARE"].should =~ /webrick/i
+ end
+
+ specify "rackup --host" do
+ run_rackup %{--host 127.0.0.1}, :host => "127.0.0.1"
+ response["REMOTE_ADDR"].should.equal "127.0.0.1"
+ end
+
+ specify "rackup --daemonize --pid" do
+ run_rackup %{--daemonize --pid testing.pid}
+ status.should.be 200
+ @rackup.should.be.eof?
+ Dir["#{root}/**/testing.pid"].should.not.be.empty?
+ end
+
+ specify "rackup --pid" do
+ run_rackup %{--pid testing.pid}
+ status.should.be 200
+ Dir["#{root}/**/testing.pid"].should.not.be.empty?
+ end
+
+ specify "rackup --version" do
+ run_rackup %{--version}, :port => false
+ output.should =~ /1.0/
+ end
+
+ specify "rackup --env development includes lint" do
+ run_rackup
+ GET("/broken_lint")
+ status.should.be 500
+ end
+
+ specify "rackup --env deployment does not include lint" do
+ run_rackup %{--env deployment}
+ GET("/broken_lint")
+ status.should.be 200
+ end
+
+ specify "rackup --env none does not include lint" do
+ run_rackup %{--env none}
+ GET("/broken_lint")
+ status.should.be 200
+ end
+
+ specify "rackup --env deployment does log" do
+ run_rackup %{--env deployment}
+ log = File.read(response["test.stderr"])
+ log.should.be.empty?
+ end
+
+ specify "rackup --env none does not log" do
+ run_rackup %{--env none}
+ GET("/")
+ log = File.read(response["test.stderr"])
+ log.should.be.empty?
+ end
+end
+rescue LoadError
+ $stderr.puts "Skipping rackup --server tests (mongrel is required). `gem install thin` and try again."
+end
\ No newline at end of file
diff --git a/vendor/gems/sanitize-1.2.1/.specification b/vendor/gems/sanitize-1.2.1/.specification
new file mode 100644
index 00000000..0dc79d6e
--- /dev/null
+++ b/vendor/gems/sanitize-1.2.1/.specification
@@ -0,0 +1,124 @@
+--- !ruby/object:Gem::Specification
+name: sanitize
+version: !ruby/object:Gem::Version
+ hash: 29
+ prerelease: false
+ segments:
+ - 1
+ - 2
+ - 1
+ version: 1.2.1
+platform: ruby
+authors:
+- Ryan Grove
+autorequire:
+bindir: bin
+cert_chain: []
+
+date: 2010-04-20 00:00:00 +02:00
+default_executable:
+dependencies:
+- !ruby/object:Gem::Dependency
+ name: nokogiri
+ prerelease: false
+ requirement: &id001 !ruby/object:Gem::Requirement
+ none: false
+ requirements:
+ - - ~>
+ - !ruby/object:Gem::Version
+ hash: 5
+ segments:
+ - 1
+ - 4
+ - 1
+ version: 1.4.1
+ type: :runtime
+ version_requirements: *id001
+- !ruby/object:Gem::Dependency
+ name: bacon
+ prerelease: false
+ requirement: &id002 !ruby/object:Gem::Requirement
+ none: false
+ requirements:
+ - - ~>
+ - !ruby/object:Gem::Version
+ hash: 19
+ segments:
+ - 1
+ - 1
+ - 0
+ version: 1.1.0
+ type: :development
+ version_requirements: *id002
+- !ruby/object:Gem::Dependency
+ name: rake
+ prerelease: false
+ requirement: &id003 !ruby/object:Gem::Requirement
+ none: false
+ requirements:
+ - - ~>
+ - !ruby/object:Gem::Version
+ hash: 63
+ segments:
+ - 0
+ - 8
+ - 0
+ version: 0.8.0
+ type: :development
+ version_requirements: *id003
+description:
+email: ryan@wonko.com
+executables: []
+
+extensions: []
+
+extra_rdoc_files: []
+
+files:
+- HISTORY
+- LICENSE
+- README.rdoc
+- lib/sanitize/config/basic.rb
+- lib/sanitize/config/relaxed.rb
+- lib/sanitize/config/restricted.rb
+- lib/sanitize/config.rb
+- lib/sanitize/version.rb
+- lib/sanitize.rb
+has_rdoc: true
+homepage: http://github.com/rgrove/sanitize/
+licenses: []
+
+post_install_message:
+rdoc_options: []
+
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+ none: false
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ hash: 59
+ segments:
+ - 1
+ - 8
+ - 6
+ version: 1.8.6
+required_rubygems_version: !ruby/object:Gem::Requirement
+ none: false
+ requirements:
+ - - ">="
+ - !ruby/object:Gem::Version
+ hash: 3
+ segments:
+ - 0
+ version: "0"
+requirements: []
+
+rubyforge_project: riposte
+rubygems_version: 1.3.7
+signing_key:
+specification_version: 3
+summary: Whitelist-based HTML sanitizer.
+test_files: []
+
diff --git a/vendor/gems/sanitize-1.2.1/HISTORY b/vendor/gems/sanitize-1.2.1/HISTORY
new file mode 100644
index 00000000..23aaf28f
--- /dev/null
+++ b/vendor/gems/sanitize-1.2.1/HISTORY
@@ -0,0 +1,90 @@
+Sanitize History
+================================================================================
+
+Version 1.2.1 (2010-04-20)
+ * Added a :remove_contents config setting. If set to true, Sanitize will
+ remove the contents of all non-whitelisted elements in addition to the
+ elements themselves. If set to an Array of element names, Sanitize will
+ remove the contents of only those elements (when filtered), and leave the
+ contents of other filtered elements. [Thanks to Rafael Souza for the Array
+ option]
+ * Added an :output_encoding config setting to allow the character encoding for
+ HTML output to be specified. The default is 'utf-8'.
+ * The environment hash passed into transformers now includes a :node_name item
+ containing the lowercase name of the current HTML node (e.g. "div").
+ * Returning anything other than a Hash or nil from a transformer will now
+ raise a meaningful Sanitize::Error exception rather than an unintended
+ NameError.
+
+Version 1.2.0 (2010-01-17)
+ * Requires Nokogiri ~> 1.4.1.
+ * Added support for transformers, which allow you to filter and alter nodes
+ using your own custom logic, on top of (or instead of) Sanitize's core
+ filter. See the README for details and examples.
+ * Added Sanitize.clean_node!, which sanitizes a Nokogiri::XML::Node and all
+ its children.
+ * Added elements
through
to the Relaxed whitelist. [Suggested by
+ David Reese]
+
+Version 1.1.0 (2009-10-11)
+ * Migrated from Hpricot to Nokogiri. Requires libxml2 >= 2.7.2 [Adam Hooper]
+ * Added an :output config setting to allow the output format to be specified.
+ Supported formats are :xhtml (the default) and :html (which outputs HTML4).
+ * Changed protocol regex to ensure Sanitize doesn't kill URLs with colons in
+ path segments. [Peter Cooper]
+
+Version 1.0.8 (2009-04-23)
+ * Added a workaround for an Hpricot bug that prevents attribute names from
+ being downcased in recent versions of Hpricot. This was exploitable to
+ prevent non-whitelisted protocols from being cleaned. [Reported by Ben
+ Wanicur]
+
+Version 1.0.7 (2009-04-11)
+ * Requires Hpricot 0.8.1+, which is finally compatible with Ruby 1.9.1.
+ * Fixed a bug that caused named character entities containing digits (like
+ ²) to be escaped when they shouldn't have been. [Reported by Sebastian
+ Steinmetz]
+
+Version 1.0.6 (2009-02-23)
+ * Removed htmlentities gem dependency.
+ * Existing well-formed character entity references in the input string are now
+ preserved rather than being decoded and re-encoded.
+ * The ' character is now encoded as ' instead of ' to prevent
+ problems in IE6.
+ * You can now specify the symbol :all in place of an element name in the
+ attributes config hash to allow certain attributes on all elements. [Thanks
+ to Mutwin Kraus]
+
+Version 1.0.5 (2009-02-05)
+ * Fixed a bug introduced in version 1.0.3 that prevented non-whitelisted
+ protocols from being cleaned when relative URLs were allowed. [Reported by
+ Dev Purkayastha]
+ * Fixed "undefined method `parent='" exceptions caused by parser changes in
+ edge Hpricot.
+
+Version 1.0.4 (2009-01-16)
+ * Fixed a bug that made it possible to sneak a non-whitelisted element through
+ by repeating it several times in a row. All versions of Sanitize prior to
+ 1.0.4 are vulnerable. [Reported by Cristobal]
+
+Version 1.0.3 (2009-01-15)
+ * Fixed a bug whereby incomplete Unicode or hex entities could be used to
+ prevent non-whitelisted protocols from being cleaned. Since IE6 and Opera
+ still decode the incomplete entities, users of those browsers may be
+ vulnerable to malicious script injection on websites using versions of
+ Sanitize prior to 1.0.3.
+
+Version 1.0.2 (2009-01-04)
+ * Fixed a bug that caused an exception to be thrown when parsing a valueless
+ attribute that's expected to contain a URL.
+
+Version 1.0.1 (2009-01-01)
+ * You can now specify :relative in a protocol config array to allow attributes
+ containing relative URLs with no protocol. The Basic and Relaxed configs
+ have been updated to allow relative URLs.
+ * Added a workaround for an Hpricot bug that causes HTML entities for
+ non-ASCII characters to be replaced by question marks, and all other
+ entities to be destructively decoded.
+
+Version 1.0.0 (2008-12-25)
+ * First release.
diff --git a/vendor/gems/sanitize-1.2.1/LICENSE b/vendor/gems/sanitize-1.2.1/LICENSE
new file mode 100644
index 00000000..bcdcc612
--- /dev/null
+++ b/vendor/gems/sanitize-1.2.1/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2010 Ryan Grove
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the 'Software'), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/gems/sanitize-1.2.1/README.rdoc b/vendor/gems/sanitize-1.2.1/README.rdoc
new file mode 100644
index 00000000..86d54da0
--- /dev/null
+++ b/vendor/gems/sanitize-1.2.1/README.rdoc
@@ -0,0 +1,333 @@
+= Sanitize
+
+Sanitize is a whitelist-based HTML sanitizer. Given a list of acceptable
+elements and attributes, Sanitize will remove all unacceptable HTML from a
+string.
+
+Using a simple configuration syntax, you can tell Sanitize to allow certain
+elements, certain attributes within those elements, and even certain URL
+protocols within attributes that contain URLs. Any HTML elements or attributes
+that you don't explicitly allow will be removed.
+
+Because it's based on Nokogiri, a full-fledged HTML parser, rather than a bunch
+of fragile regular expressions, Sanitize has no trouble dealing with malformed
+or maliciously-formed HTML, and will always output valid HTML or XHTML.
+
+*Author*:: Ryan Grove (mailto:ryan@wonko.com)
+*Version*:: 1.2.1 (2010-04-20)
+*Copyright*:: Copyright (c) 2010 Ryan Grove. All rights reserved.
+*License*:: MIT License (http://opensource.org/licenses/mit-license.php)
+*Website*:: http://github.com/rgrove/sanitize
+
+== Requires
+
+* Nokogiri ~> 1.4.1
+* libxml2 >= 2.7.2
+
+== Installation
+
+Latest stable release:
+
+ gem install sanitize
+
+Latest development version:
+
+ gem install sanitize --pre
+
+== Usage
+
+If you don't specify any configuration options, Sanitize will use its strictest
+settings by default, which means it will strip all HTML and leave only text
+behind.
+
+ require 'rubygems'
+ require 'sanitize'
+
+ html = 'foo'
+
+ Sanitize.clean(html) # => 'foo'
+
+== Configuration
+
+In addition to the ultra-safe default settings, Sanitize comes with three other
+built-in modes.
+
+=== Sanitize::Config::RESTRICTED
+
+Allows only very simple inline formatting markup. No links, images, or block
+elements.
+
+ Sanitize.clean(html, Sanitize::Config::RESTRICTED) # => 'foo'
+
+=== Sanitize::Config::BASIC
+
+Allows a variety of markup including formatting tags, links, and lists. Images
+and tables are not allowed, links are limited to FTP, HTTP, HTTPS, and mailto
+protocols, and a rel="nofollow" attribute is added to all links to
+mitigate SEO spam.
+
+ Sanitize.clean(html, Sanitize::Config::BASIC)
+ # => 'foo'
+
+=== Sanitize::Config::RELAXED
+
+Allows an even wider variety of markup than BASIC, including images and tables.
+Links are still limited to FTP, HTTP, HTTPS, and mailto protocols, while images
+are limited to HTTP and HTTPS. In this mode, rel="nofollow" is not
+added to links.
+
+ Sanitize.clean(html, Sanitize::Config::RELAXED)
+ # => 'foo'
+
+=== Custom Configuration
+
+If the built-in modes don't meet your needs, you can easily specify a custom
+configuration:
+
+ Sanitize.clean(html, :elements => ['a', 'span'],
+ :attributes => {'a' => ['href', 'title'], 'span' => ['class']},
+ :protocols => {'a' => {'href' => ['http', 'https', 'mailto']}})
+
+==== :add_attributes (Hash)
+
+Attributes to add to specific elements. If the attribute already exists, it will
+be replaced with the value specified here. Specify all element names and
+attributes in lowercase.
+
+ :add_attributes => {
+ 'a' => {'rel' => 'nofollow'}
+ }
+
+==== :attributes (Hash)
+
+Attributes to allow for specific elements. Specify all element names and
+attributes in lowercase.
+
+ :attributes => {
+ 'a' => ['href', 'title'],
+ 'blockquote' => ['cite'],
+ 'img' => ['alt', 'src', 'title']
+ }
+
+If you'd like to allow certain attributes on all elements, use the symbol
+:all instead of an element name.
+
+ :attributes => {
+ :all => ['class'],
+ 'a' => ['href', 'title']
+ }
+
+==== :allow_comments (boolean)
+
+Whether or not to allow HTML comments. Allowing comments is strongly
+discouraged, since IE allows script execution within conditional comments. The
+default value is false.
+
+==== :elements (Array)
+
+Array of element names to allow. Specify all names in lowercase.
+
+ :elements => [
+ 'a', 'b', 'blockquote', 'br', 'cite', 'code', 'dd', 'dl', 'dt', 'em',
+ 'i', 'li', 'ol', 'p', 'pre', 'q', 'small', 'strike', 'strong', 'sub',
+ 'sup', 'u', 'ul'
+ ]
+
+==== :output (Symbol)
+
+Output format. Supported formats are :html and :xhtml,
+defaulting to :xhtml.
+
+==== :output_encoding (String)
+
+Character encoding to use for HTML output. Default is 'utf-8'.
+
+==== :protocols (Hash)
+
+URL protocols to allow in specific attributes. If an attribute is listed here
+and contains a protocol other than those specified (or if it contains no
+protocol at all), it will be removed.
+
+ :protocols => {
+ 'a' => {'href' => ['ftp', 'http', 'https', 'mailto']},
+ 'img' => {'src' => ['http', 'https']}
+ }
+
+If you'd like to allow the use of relative URLs which don't have a protocol,
+include the symbol :relative in the protocol array:
+
+ :protocols => {
+ 'a' => {'href' => ['http', 'https', :relative]}
+ }
+
+==== :remove_contents (boolean or Array)
+
+If set to +true+, Sanitize will remove the contents of any non-whitelisted
+elements in addition to the elements themselves. By default, Sanitize leaves the
+safe parts of an element's contents behind when the element is removed.
+
+If set to an Array of element names, then only the contents of the specified
+elements (when filtered) will be removed, and the contents of all other filtered
+elements will be left behind.
+
+The default value is false.
+
+==== :transformers
+
+See below.
+
+=== Transformers
+
+Transformers allow you to filter and alter nodes using your own custom logic, on
+top of (or instead of) Sanitize's core filter. A transformer is any object that
+responds to call() (such as a lambda or proc) and returns either
+nil or a Hash containing certain optional response values.
+
+To use one or more transformers, pass them to the :transformers
+config setting:
+
+ Sanitize.clean(html, :transformers => [transformer_one, transformer_two])
+
+==== Input
+
+Each registered transformer's call() method will be called once for
+each element node in the HTML, and will receive as an argument an environment
+Hash that contains the following items:
+
+[:config]
+ The current Sanitize configuration Hash.
+
+[:node]
+ A Nokogiri::XML::Node object representing an HTML element.
+
+[:node_name]
+ The name of the current HTML node, always lowercase (e.g. "div" or "span").
+
+==== Processing
+
+Each transformer has full access to the Nokogiri::XML::Node that's passed into
+it and to the rest of the document via the node's document()
+method. Any changes will be reflected instantly in the document and passed on to
+subsequently-called transformers and to Sanitize itself. A transformer may even
+call Sanitize internally to perform custom sanitization if needed.
+
+Nodes are passed into transformers in the order in which they're traversed. It's
+important to note that Nokogiri traverses markup from the deepest node upward,
+not from the first node to the last node:
+
+ html = '
foo
'
+ transformer = lambda{|env| puts env[:node].name }
+
+ # Prints "span", then "div".
+ Sanitize.clean(html, :transformers => transformer)
+
+Transformers have a tremendous amount of power, including the power to
+completely bypass Sanitize's built-in filtering. Be careful!
+
+==== Output
+
+A transformer may return either +nil+ or a Hash. A return value of +nil+
+indicates that the transformer does not wish to act on the current node in any
+way. A returned Hash may contain the following items, all of which are optional:
+
+[:attr_whitelist]
+ Array of attribute names to add to the whitelist for the current node, in
+ addition to any whitelisted attributes already defined in the current config.
+
+[:node]
+ A Nokogiri::XML::Node object that should replace the current node. All
+ subsequent transformers and Sanitize itself will receive this new node.
+
+[:whitelist]
+ If _true_, the current node (and only the current node) will be whitelisted,
+ regardless of the current Sanitize config.
+
+[:whitelist_nodes]
+ Array of specific Nokogiri::XML::Node objects to whitelist, anywhere in the
+ document, regardless of the current Sanitize config.
+
+==== Example: Transformer to whitelist YouTube video embeds
+
+The following example demonstrates how to create a Sanitize transformer that
+will safely whitelist valid YouTube video embeds without having to blindly allow
+other kinds of embedded content, which would be the case if you tried to do this
+by just whitelisting all , , and
+ elements:
+
+ lambda do |env|
+ node = env[:node]
+ node_name = env[:node_name]
+ parent = node.parent
+
+ # Since the transformer receives the deepest nodes first, we look for a
+ # element or an