forgot to add these

This commit is contained in:
Reinier Balt 2012-04-05 22:21:28 +02:00
parent 86afd42148
commit fde64e0b3d
145 changed files with 9044 additions and 0 deletions

View file

@ -0,0 +1,65 @@
source :gemcutter
source "http://gems.github.com/"
gem "rake", "~>0.8.7"
gem "rails", "~>2.3.12"
gem "highline", "~>1.5.0"
gem "RedCloth", "4.2.8"
gem "sanitize", "~>1.2.1"
gem "rack", "1.1.0"
gem "will_paginate", "~> 2.3.15"
gem "has_many_polymorphs", "~> 2.13"
gem "acts_as_list", "~>0.1.4"
gem "aasm", "~>2.2.0"
gem "rubyjedi-actionwebservice", :require => "actionwebservice"
gem "rubycas-client", "~>2.2.1"
gem "ruby-openid", :require => "openid"
# you may comment out the database driver you will not be using.
# This will prevent a native build of the driver. Building native drivers is not always possible on all hosters
gem "sqlite3"
gem "mysql"
gem 'bcrypt-ruby', '~> 2.1.4'
gem 'htmlentities', '~> 4.3.0'
gem "mail"
if RUBY_VERSION.to_f >= 1.9
gem "soap4r-ruby1.9"
else
gem "soap4r", "~>1.5.8"
end
group :development do
if RUBY_VERSION.to_f >= 1.9
gem "ruby-debug19"
gem "mongrel", "1.2.0.pre2"
else
gem "ruby-debug"
gem "mongrel"
end
gem "yard"
end
group :test do
gem "test-unit", "1.2.3"
gem "flexmock"
gem "ZenTest", ">=4.0.0"
gem "hpricot"
gem "hoe"
gem "rspec-rails", "~>1.3.3"
gem "thoughtbot-factory_girl"
gem 'memory_test_fix', '~>0.1.3'
gem "capybara", ">=0.3.5"
gem "selenium-webdriver" # Note that > 2.14 has problems: https://code.google.com/p/selenium/issues/detail?id=3075
gem "database_cleaner", ">=0.5.0"
gem "cucumber-rails", "~>0.3.2"
gem "aruba", "0.2.2", :path => "vendor/gems/aruba-0.2.2"
# uncomment to use the webkit option. This depends on Qt to be installed
#gem "capybara-webkit"
# uncomment to be able to make screenshots from scenarios
#gem "capybara-screenshot"
#gem "launchy"
end

View file

@ -0,0 +1,39 @@
source 'https://rubygems.org'
gem 'rails', '3.2.3'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'sqlite3'
gem 'mysql'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', :platform => :ruby
gem 'uglifier', '>= 1.0.3'
end
gem 'jquery-rails'
# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'
# To use Jbuilder templates for JSON
# gem 'jbuilder'
# Use unicorn as the app server
# gem 'unicorn'
# Deploy with Capistrano
# gem 'capistrano'
# To use debugger
# gem 'ruby-debug19', :require => 'ruby-debug'

View file

@ -0,0 +1,22 @@
class TodoApi < ActionWebService::API::Base
api_method :new_todo,
:expects => [{:username => :string}, {:token => :string}, {:context_id => :int}, {:description => :string}, {:notes => :string}],
:returns => [:int]
api_method :new_todo_for_project,
:expects => [{:username => :string}, {:token => :string}, {:context_id => :int}, {:project_id => :int}, {:description => :string}, {:notes => :string}],
:returns => [:int]
api_method :new_rich_todo,
:expects => [{:username => :string}, {:token => :string}, {:default_context_id => :int}, {:description => :string}, {:notes => :string}],
:returns => [:int]
api_method :list_contexts,
:expects => [{:username => :string}, {:token => :string}],
:returns => [[Context]]
api_method :list_projects,
:expects => [{:username => :string}, {:token => :string}],
:returns => [[Project]]
end

View file

@ -0,0 +1,301 @@
# The methods added to this helper will be available to all templates in the
# application.
module ApplicationHelper
# Replicates the link_to method but also checks request.request_uri to find
# current page. If that matches the url, the link is marked id = "current"
#
def navigation_link(name, options = {}, html_options = nil, *parameters_for_method_reference)
if html_options
html_options = html_options.stringify_keys
convert_options_to_javascript!(html_options)
tag_options = tag_options(html_options)
else
tag_options = nil
end
url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference)
id_tag = (request.request_uri == url) ? " id=\"current\"" : ""
"<a href=\"#{url}\"#{tag_options}#{id_tag}>#{name || url}</a>"
end
def days_from_today(date)
date.in_time_zone.to_date - current_user.time.to_date
end
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code
#
def due_date(due)
return "" if due.nil?
days = days_from_today(due)
colors = ['amber','amber','orange','orange','orange','orange','orange','orange']
color = :red if days < 0
color = :green if days > 7
color = colors[days] if color.nil?
return content_tag(:a, {:title => format_date(due)}) {
content_tag(:span, {:class => color}) {
case days
when 0
t('todos.next_actions_due_date.due_today')
when 1
t('todos.next_actions_due_date.due_tomorrow')
when 2..7
if prefs.due_style == Preference.due_styles[:due_on]
# TODO: internationalize strftime here
t('models.preference.due_on', :date => due.strftime("%A"))
else
t('models.preference.due_in', :days => days)
end
else
# overdue or due very soon! sound the alarm!
if days == -1
t('todos.next_actions_due_date.overdue_by', :days => days * -1)
elsif days < -1
t('todos.next_actions_due_date.overdue_by_plural', :days => days * -1)
else
# more than a week away - relax
t('models.preference.due_in', :days => days)
end
end
}
}
end
# Check due date in comparison to today's date Flag up date appropriately with
# a 'traffic light' colour code Modified method for mobile screen
#
def due_date_mobile(due)
if due == nil
return ""
end
days = days_from_today(due)
case days
when 0
"<span class=\"amber\">"+ format_date(due) + "</span>"
when 1
"<span class=\"amber\">" + format_date(due) + "</span>"
# due 2-7 days away
when 2..7
"<span class=\"orange\">" + format_date(due) + "</span>"
else
# overdue or due very soon! sound the alarm!
if days < 0
"<span class=\"red\">" + format_date(due) +"</span>"
else
# more than a week away - relax
"<span class=\"green\">" + format_date(due) + "</span>"
end
end
end
# Returns a count of next actions in the given context or project. The result
# is count and a string descriptor, correctly pluralised if there are no
# actions or multiple actions
#
def count_undone_todos_phrase(todos_parent, string="actions")
@controller.count_undone_todos_phrase(todos_parent, string)
end
def count_undone_todos_phrase_text(todos_parent, string="actions")
count_undone_todos_phrase(todos_parent, string).gsub("&nbsp;"," ")
end
def count_undone_todos_and_notes_phrase(project, string="actions")
s = count_undone_todos_phrase(project, string)
s += ", #{pluralize(project.note_count, 'note')}" unless project.note_count == 0
s
end
def link_to_context(context, descriptor = sanitize(context.name))
link_to( descriptor, context, :title => "View context: #{context.name}" )
end
def link_to_project(project, descriptor = sanitize(project.name))
link_to( descriptor, project, :title => "View project: #{project.name}" )
end
def link_to_edit_note (note, descriptor = sanitize(note.id.to_s))
link_to(descriptor,
url_for({:controller => 'notes', :action => 'edit', :id => note.id}),
{:id => "link_edit_#{dom_id(note)}", :class => "note_edit_settings"})
end
def link_to_project_mobile(project, accesskey, descriptor = sanitize(project.name))
link_to( descriptor, project_path(project, :format => 'm'), {:title => "View project: #{project.name}", :accesskey => accesskey} )
end
def item_link_to_context(item)
descriptor = "[C]"
descriptor = "[#{item.context.name}]" if prefs.verbose_action_descriptors
link_to_context( item.context, descriptor )
end
def item_link_to_project(item)
descriptor = "[P]"
descriptor = "[#{item.project.name}]" if prefs.verbose_action_descriptors
link_to_project( item.project, descriptor )
end
def render_flash
render :partial => 'shared/flash', :object => flash
end
def recurrence_time_span(rt)
case rt.ends_on
when "no_end_date"
return rt.start_from.nil? ? "" : I18n.t("todos.recurrence.pattern.from") + " " + format_date(rt.start_from)
when "ends_on_number_of_times"
return I18n.t("todos.recurrence.pattern.times", :number => rt.number_of_occurences)
when "ends_on_end_date"
starts = rt.start_from.nil? ? "" : I18n.t("todos.recurrence.pattern.from") + " " + format_date(rt.start_from)
ends = rt.end_date.nil? ? "" : " " + I18n.t("todos.recurrence.pattern.until") + " " + format_date(rt.end_date)
return starts+ends
else
raise Exception.new, "unknown recurrence time span selection (#{rt.ends_on})"
end
end
def recurrence_pattern_as_text(recurring_todo)
rt = recurring_todo.recurring_target_as_text
rp = recurring_todo.recurrence_pattern
# only add space if recurrence_pattern has content
rp = " " + rp if !rp.nil?
rts = recurrence_time_span(recurring_todo)
# only add space if recurrence_time_span has content
rts = " " + rts if !(rts == "")
return rt+rp+rts
end
def date_format_for_date_picker()
standard_format = current_user.prefs.date_format
translations = [
['%m', 'mm'],
['%b', 'M'],
['%B', 'MM'],
['%d', 'dd'],
['%a', 'D'],
['%A', 'DD'],
['%y', 'y'],
['%Y', 'yy']
]
translations.inject(standard_format) do |str, translation|
str.gsub(*translation)
end
end
AUTO_LINK_MESSAGE_RE = %r{message://<[^>]+>} unless const_defined?(:AUTO_LINK_MESSAGE_RE)
# Converts message:// links to href. This URL scheme is used on Mac OS X
# to link to a mail message in Mail.app.
def auto_link_message(text)
text.gsub(AUTO_LINK_MESSAGE_RE) do
href = $&
left, right = $`, $'
# detect already linked URLs and URLs in the middle of a tag
if left =~ /<[^>]+$/ && right =~ /^[^>]*>/
# do not change string; URL is alreay linked
href
else
content = content_tag(:a, h(href), :href => h(href))
end
end
end
def format_note(note)
note = auto_link_message(note)
note = markdown(note)
note = auto_link(note, :link => :urls)
# add onenote and message protocols
Sanitize::Config::RELAXED[:protocols]['a']['href'] << 'onenote'
Sanitize::Config::RELAXED[:protocols]['a']['href'] << 'message'
note = Sanitize.clean(note, Sanitize::Config::RELAXED)
return note
end
def sidebar_html_for_titled_list (list, title)
return content_tag(:h3, title+" (#{list.length})") +
content_tag(:ul, sidebar_html_for_list(list))
end
def sidebar_html_for_list(list)
if list.empty?
return content_tag(:li, t('sidebar.list_empty'))
else
return list.inject("") do |html, item|
link = (item.class == "Project") ? link_to_project( item ) : link_to_context(item)
html << content_tag(:li, link + " (" + count_undone_todos_phrase(item,"actions")+")")
end
end
end
def generate_i18n_strings
js = "i18n_locale='#{I18n.locale}';\n"
js << "i18n = new Array();\n"
%w{
shared.toggle_multi shared.toggle_multi_title
shared.hide_form shared.hide_action_form_title
shared.toggle_single shared.toggle_single_title
projects.hide_form projects.hide_form_title
projects.show_form projects.show_form_title
contexts.hide_form contexts.hide_form_title
contexts.show_form contexts.show_form_title
contexts.new_context_pre contexts.new_context_post
common.cancel common.ok
common.ajaxError todos.unresolved_dependency
}.each do |s|
js << "i18n['#{s}'] = '#{ t(s).gsub(/'/, "\\\\'") }';\n"
end
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
def determine_done_path
case @controller.controller_name
when "contexts"
done_todos_context_path(@context)
when "projects"
done_todos_project_path(@project)
when "todos"
if source_view_is(:tag)
done_tag_path(@tag_name)
else
done_todos_path
end
else
done_todos_path
end
end
def determine_all_done_path
case @controller.controller_name
when "contexts"
all_done_todos_context_path(@context)
when "projects"
all_done_todos_project_path(@project)
when "todos"
if source_view_is(:tag)
all_done_tag_path(@tag_name)
else
all_done_todos_path
end
else
all_done_todos_path
end
end
end

View file

@ -0,0 +1,37 @@
# MySQL. Versions 4.1 and 5.0 are recommended.
#
#
# Be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
development:
adapter: mysql
database: tracks_trunk
encoding: utf8
host: localhost
username: tracks
password: 32tracks55
mdevelopment:
adapter: sqlite3
database: db/tracks-21-test.sqlite3.db
test: &TEST
# adapter: sqlite3
# database: ":memory:"
# verbosity: quiet
adapter: mysql
database: tracks_test
host: localhost
username: tracks
password: 32tracks55
production:
adapter: mysql
database: tracks_trunk
encoding: utf8
host: localhost
username: tracks
password: 32tracks55
cucumber:
<<: *TEST

View file

@ -0,0 +1,19 @@
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false
# Unique cookies
config.action_controller.session = { :key => 'TracksDev' }
NOTIFY_BAR="<div id=\"develop-notify-bar\">&nbsp;</div>"

View file

@ -0,0 +1,119 @@
# Be sure to restart your webserver when you modify this file.
# Uncomment below to force Rails into production mode
# (Use only when you can't set environment variables through your web/app server)
# ENV['RAILS_ENV'] = 'production'
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
require 'yaml'
SITE_CONFIG = YAML.load_file(File.join(File.dirname(__FILE__), 'site.yml'))
class Rails::Configuration
attr_accessor :action_web_service
end
Encoding.default_external = Encoding::UTF_8 if RUBY_VERSION > "1.9"
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
# config.frameworks -= [ :action_web_service, :action_mailer ]
config.autoload_paths += %W( #{RAILS_ROOT}/app/apis )
config.action_controller.use_accept_header = true
# Use the database for sessions instead of the file system
# (create the session table with 'rake create_sessions_table')
config.action_controller.session_store = :active_record_store
config.action_controller.session = {
:key => '_tracks_session_id',
:secret => SITE_CONFIG['salt'] * (30.0 / SITE_CONFIG['salt'].length).ceil #must be at least 30 characters
}
config.action_controller.relative_url_root = SITE_CONFIG['subdir'] if SITE_CONFIG['subdir']
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
# config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
# Make Active Record use UTC-base instead of local time
config.active_record.default_timezone = :utc
# You''ll probably want to change this to the time zone of the computer where Tracks is running
# run rake time:zones:local have Rails suggest time zone names on your system
config.time_zone = SITE_CONFIG['time_zone']
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
config.active_record.schema_format = :ruby
# allow other protocols in urls for sanitzer. Add to your liking, for example
# config.action_view.sanitized_allowed_protocols = 'onenote', 'blah', 'proto'
# to enable "link":onenote://... or "link":blah://... hyperlinks
config.action_view.sanitized_allowed_protocols = 'onenote', 'message'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
end
# Add new inflection rules using the following format
# (all these examples are active by default):
# Inflector.inflections do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# Include your application configuration below
require 'name_part_finder'
require 'tracks/todo_list'
require 'tracks/config'
require 'tagging_extensions' # Needed for tagging-specific extensions
require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557
if ( SITE_CONFIG['authentication_schemes'].include? 'ldap')
require 'net/ldap' #requires ruby-net-ldap gem be installed
require 'simple_ldap_authenticator'
ldap = SITE_CONFIG['ldap']
SimpleLdapAuthenticator.ldap_library = ldap['library']
SimpleLdapAuthenticator.servers = ldap['servers']
SimpleLdapAuthenticator.use_ssl = ldap['ssl']
SimpleLdapAuthenticator.login_format = ldap['login_format']
end
if ( SITE_CONFIG['authentication_schemes'].include? 'open_id')
#requires ruby-openid gem to be installed
OpenID::Util.logger = RAILS_DEFAULT_LOGGER
end
if ( SITE_CONFIG['authentication_schemes'].include? 'cas')
#requires rubycas-client gem to be installed
if defined? CASClient
require 'casclient/frameworks/rails/filter'
CASClient::Frameworks::Rails::Filter.configure(
:cas_base_url => SITE_CONFIG['cas_server'] ,
:cas_server_logout => SITE_CONFIG['cas_server_logout']
)
end
end
# changed in development.rb to show under_construction bar
NOTIFY_BAR = "" unless defined?(NOTIFY_BAR)
tracks_version='2.2devel'
# 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`
tracks_version=tracks_version + ' ('+info+')'
TRACKS_VERSION=tracks_version

View file

View file

@ -0,0 +1,113 @@
module AuthenticatedTestHelper
# Sets the current user in the session from the user fixtures.
def login_as(user)
@request.session['user_id'] = user ? users(user).id : nil
end
def content_type(type)
@request.env['Content-Type'] = type
end
def accept(accept)
@request.env["HTTP_ACCEPT"] = accept
end
def authorize_as(user)
if user
@request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}"
accept 'application/xml'
content_type 'application/xml'
else
@request.env["HTTP_AUTHORIZATION"] = nil
accept nil
content_type nil
end
end
# http://project.ioni.st/post/217#post-217
#
# def test_new_publication
# assert_difference(Publication, :count) do
# post :create, :publication => {...}
# # ...
# end
# end
#
def assert_difference(object, method = nil, difference = 1)
initial_value = object.send(method)
yield
assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
end
def assert_no_difference(object, method, &block)
assert_difference object, method, 0, &block
end
# Assert the block redirects to the login
#
# assert_requires_login(:bob) { |c| c.get :edit, :id => 1 }
#
def assert_requires_login(login = nil)
yield HttpLoginProxy.new(self, login)
end
def assert_http_authentication_required(login = nil)
yield XmlLoginProxy.new(self, login)
end
def reset!(*instance_vars)
instance_vars = [:controller, :request, :response] unless instance_vars.any?
instance_vars.collect! { |v| "@#{v}".to_sym }
instance_vars.each do |var|
instance_variable_set(var, instance_variable_get(var).class.new)
end
end
end
class BaseLoginProxy
attr_reader :controller
attr_reader :options
def initialize(controller, login)
@controller = controller
@login = login
end
private
def authenticated
raise NotImplementedError
end
def check
raise NotImplementedError
end
def method_missing(method, *args)
@controller.reset!
authenticate
@controller.send(method, *args)
check
end
end
class HttpLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.login_as @login if @login
end
def check
@controller.assert_redirected_to :controller => 'account', :action => 'login'
end
end
class XmlLoginProxy < BaseLoginProxy
protected
def authenticate
@controller.accept 'application/xml'
@controller.authorize_as @login if @login
end
def check
@controller.assert_response 401
end
end

View file

@ -0,0 +1,222 @@
require_dependency "user"
module LoginSystem
def current_user
get_current_user
end
def prefs
current_user.prefs unless current_user.nil?
end
# Logout the {#current_user} and redirect to login page
#
# @param [String] message notification to display
def logout_user message=t('login.logged_out')
@user.forget_me if logged_in?
cookies.delete :auth_token
session['user_id'] = nil
if ( SITE_CONFIG['authentication_schemes'].include? 'cas') && session[:cas_user]
CASClient::Frameworks::Rails::Filter.logout(self)
else
reset_session
notify :notice, message
redirect_to_login
end
end
protected
# overwrite this if you want to restrict access to only a few actions
# or if you want to check if the user has the correct rights
# example:
#
# # only allow nonbobs
# def authorize?(user)
# user.login != "bob"
# end
def authorize?(user)
true
end
# overwrite this method if you only want to protect certain actions of the controller
# example:
#
# # don't protect the login and the about method
# def protect?(action)
# if ['action', 'about'].include?(action)
# return false
# else
# return true
# end
# end
def protect?(action)
true
end
# When called with before_filter :login_from_cookie will check for an :auth_token
# cookie and log the user back in if appropriate
def login_from_cookie
return unless cookies[:auth_token] && !logged_in?
token = cookies[:auth_token]
user = User.where(:remember_token => token)
if user && user.remember_token?
session['user_id'] = user.id
set_current_user(user)
current_user.remember_me
cookies[:auth_token] = { :value => current_user.remember_token , :expires => current_user.remember_token_expires_at, :secure => SITE_CONFIG['secure_cookies'] }
flash[:notice] = t('login.successful')
end
end
def login_or_feed_token_required
if ['rss', 'atom', 'txt', 'ics'].include?(params[:format])
if user = User.find_by_token(params[:token])
set_current_user(user)
return true
end
end
login_required
end
# login_required filter. add
#
# before_filter :login_required
#
# if the controller should be under any rights management.
# for finer access control you can overwrite
#
# def authorize?(user)
#
def login_required
if not protect?(action_name)
return true
end
login_from_cookie
if session['user_id'] and authorize?(get_current_user)
return true
end
http_user, http_pass = get_basic_auth_data
if user = User.authenticate(http_user, http_pass)
session['user_id'] = user.id
set_current_user(user)
return true
end
# store current location so that we can
# come back after the user logged in
store_location unless params[:format] == 'js'
# call overwriteable reaction to unauthorized access
access_denied
return false
end
def login_optional
login_from_cookie
if session['user_id'] and authorize?(get_current_user)
return true
end
http_user, http_pass = get_basic_auth_data
if user = User.authenticate(http_user, http_pass)
session['user_id'] = user.id
set_current_user(user)
return true
end
return true
end
def logged_in?
current_user != nil
end
def get_current_user
if @user.nil? && session['user_id']
@user = User.find(session['user_id'])
end
@user
end
def set_current_user(user)
@user = user
end
# overwrite if you want to have special behavior in case the user is not authorized
# to access the current operation.
# the default action is to redirect to the login screen
# example use :
# a popup window might just close itself for instance
def access_denied
respond_to do |format|
format.html { redirect_to login_path }
format.m { redirect_to formatted_login_path(:format => 'm') }
format.js { render :partial => 'login/redirect_to_login' }
format.xml { basic_auth_denied }
format.rss { basic_auth_denied }
format.atom { basic_auth_denied }
format.text { basic_auth_denied }
end
end
# store current uri in the session.
# we can return to this location by calling return_location
def store_location
session['return-to'] = request.request_uri
end
# move to the last store_location call or to the passed default one
def redirect_back_or_default(default)
if session['return-to'].nil?
redirect_to default
else
redirect_to session['return-to']
session['return-to'] = nil
end
end
# HTTP Basic auth code adapted from Coda Hale's simple_http_auth plugin. Thanks, Coda!
def get_basic_auth_data
auth_locations = ['REDIRECT_REDIRECT_X_HTTP_AUTHORIZATION',
'REDIRECT_X_HTTP_AUTHORIZATION',
'X-HTTP_AUTHORIZATION', 'HTTP_AUTHORIZATION']
authdata = nil
for location in auth_locations
if request.env.has_key?(location)
authdata = request.env[location].to_s.split
end
end
if authdata and authdata[0] == 'Basic'
user, pass = Base64.decode64(authdata[1]).split(':')[0..1]
else
user, pass = ['', '']
end
return user, pass
end
def basic_auth_denied
response.headers["WWW-Authenticate"] = "Basic realm=\"'Tracks Login Required'\""
render :text => t('login.unsuccessful'), :status => 401
end
private
# Redirect the user to the login page.
def redirect_to_login
respond_to do |format|
format.html { redirect_to login_path }
format.m { redirect_to login_path(:format => 'm') }
end
end
end

View file

@ -0,0 +1,5 @@
module NamePartFinder
def find_by_namepart(namepart)
find_by_name(namepart) || find(:first, :conditions => ["name LIKE ?", namepart + '%'])
end
end

View file

@ -0,0 +1,200 @@
class ActiveRecord::Base #:nodoc:
# These extensions make models taggable. This file is automatically generated and required by your app if you run the tagging generator included with has_many_polymorphs.
module TaggingExtensions
# Add tags to <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
#
# We need to avoid name conflicts with the built-in ActiveRecord association methods, thus the underscores.
def _add_tags incoming
taggable?(true)
tag_cast_to_string(incoming).each do |tag_name|
# added following check to prevent empty tags from being saved (which will fail)
unless tag_name.blank?
begin
tag = Tag.find_or_create_by_name(tag_name)
raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record?
tags << tag
rescue ActiveRecord::StatementInvalid => e
raise unless e.to_s =~ /duplicate/i
end
end
end
end
# Removes tags from <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
def _remove_tags outgoing
taggable?(true)
outgoing = tag_cast_to_string(outgoing)
tags.delete(*(tags.select do |tag|
outgoing.include? tag.name
end))
end
# Returns the tags on <tt>self</tt> as a string.
def tag_list
# Redefined later to avoid an RDoc parse error.
end
# Replace the existing tags on <tt>self</tt>. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags.
def tag_with list
#:stopdoc:
taggable?(true)
list = tag_cast_to_string(list)
# Transactions may not be ideal for you here; be aware.
Tag.transaction do
current = tags.map(&:name)
_add_tags(list - current)
_remove_tags(current - list)
end
self
#:startdoc:
end
# Returns the tags on <tt>self</tt> as a string.
def tag_list #:nodoc:
#:stopdoc:
taggable?(true)
tags.reload
tags.to_s
#:startdoc:
end
def tag_list=(value)
tag_with(value)
end
private
def tag_cast_to_string obj #:nodoc:
case obj
when Array
obj.map! do |item|
case item
# removed next line as it prevents using numbers as tags
# when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot.
when Tag then item.name
when String then item
else
raise "Invalid type"
end
end
when String
obj = obj.split(Tag::DELIMITER).map do |tag_name|
tag_name.strip.squeeze(" ")
end
else
raise "Invalid object of class #{obj.class} as tagging method parameter"
end.flatten.compact.map(&:downcase).uniq
end
# Check if a model is in the :taggables target list. The alternative to this check is to explicitly include a TaggingMethods module (which you would create) in each target model.
def taggable?(should_raise = false) #:nodoc:
unless flag = respond_to?(:tags)
raise "#{self.class} is not a taggable model" if should_raise
end
flag
end
end
module TaggingFinders
# Find all the objects tagged with the supplied list of tags
#
# Usage : Model.tagged_with("ruby")
# Model.tagged_with("hello", "world")
# Model.tagged_with("hello", "world", :limit => 10)
#
# XXX This query strategy is not performant, and needs to be rewritten as an inverted join or a series of unions
#
def tagged_with(*tag_list)
options = tag_list.last.is_a?(Hash) ? tag_list.pop : {}
tag_list = parse_tags(tag_list)
scope = scope(:find)
options[:select] ||= "#{table_name}.*"
options[:from] ||= "#{table_name}, tags, taggings"
sql = "SELECT #{(scope && scope[:select]) || options[:select]} "
sql << "FROM #{(scope && scope[:from]) || options[:from]} "
add_joins!(sql, options[:joins], scope)
sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id "
sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' "
sql << "AND taggings.tag_id = tags.id "
tag_list_condition = tag_list.map {|name| "'#{name}'"}.join(", ")
sql << "AND (tags.name IN (#{sanitize_sql(tag_list_condition)})) "
sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
columns = column_names.map do |column|
"#{table_name}.#{column}"
end.join(", ")
sql << "GROUP BY #{columns} "
sql << "HAVING COUNT(taggings.tag_id) = #{tag_list.size}"
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope)
add_lock!(sql, options, scope)
find_by_sql(sql)
end
def self.tagged_with_any(*tag_list)
options = tag_list.last.is_a?(Hash) ? tag_list.pop : {}
tag_list = parse_tags(tag_list)
scope = scope(:find)
options[:select] ||= "#{table_name}.*"
options[:from] ||= "#{table_name}, meta_tags, taggings"
sql = "SELECT #{(scope && scope[:select]) || options[:select]} "
sql << "FROM #{(scope && scope[:from]) || options[:from]} "
add_joins!(sql, options, scope)
sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id "
sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' "
sql << "AND taggings.meta_tag_id = meta_tags.id "
sql << "AND ("
or_options = []
tag_list.each do |name|
or_options << "meta_tags.name = '#{name}'"
end
or_options_joined = or_options.join(" OR ")
sql << "#{or_options_joined}) "
sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
columns = column_names.map do |column|
"#{table_name}.#{column}"
end.join(", ")
sql << "GROUP BY #{columns} "
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope)
add_lock!(sql, options, scope)
find_by_sql(sql)
end
def parse_tags(tags)
return [] if tags.blank?
tags = Array(tags).first
tags = tags.respond_to?(:flatten) ? tags.flatten : tags.split(Tag::DELIMITER)
tags.map { |tag| tag.strip.squeeze(" ") }.flatten.compact.map(&:downcase).uniq
end
end
include TaggingExtensions
extend TaggingFinders
end

View file

View file

@ -0,0 +1,38 @@
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# It is recommended to regenerate this file in the future when you upgrade to a
# newer version of cucumber-rails. Consider adding your own code to a new file
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.
vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil?
begin
require 'cucumber/rake/task'
namespace :cucumber do
Cucumber::Rake::Task.new({:selenium => :env_to_selenium}, 'Run features that require selenium') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'selenium'
end
Cucumber::Rake::Task.new({:selenium_wip => :env_to_selenium}, 'Run unfinished features that require selenium') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'selenium_wip'
end
task :env_to_selenium => 'db:test:prepare' do
ENV['RAILS_ENV'] = 'selenium'
end
desc 'Run all features'
task :all => [:ok, :wip, :selenium, :selenium_wip]
end
rescue LoadError
desc 'cucumber rake task not available (cucumber not installed)'
task :cucumber do
abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
end
end

View file

@ -0,0 +1,53 @@
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# It is recommended to regenerate this file in the future when you upgrade to a
# newer version of cucumber-rails. Consider adding your own code to a new file
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.
unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks
vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil?
begin
require 'cucumber/rake/task'
namespace :cucumber do
Cucumber::Rake::Task.new({:ok => 'db:test:prepare'}, 'Run features that should pass') do |t|
t.binary = vendored_cucumber_bin # If nil, the gem's binary is used.
t.fork = true # You may get faster startup if you set this to false
t.profile = 'default'
end
Cucumber::Rake::Task.new({:wip => 'db:test:prepare'}, 'Run features that are being worked on') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'wip'
end
Cucumber::Rake::Task.new({:rerun => 'db:test:prepare'}, 'Record failing features and run only them if any exist') do |t|
t.binary = vendored_cucumber_bin
t.fork = true # You may get faster startup if you set this to false
t.profile = 'rerun'
end
desc 'Run all features'
task :all => [:ok, :wip]
end
desc 'Alias for cucumber:ok'
task :cucumber => 'cucumber:ok'
task :default => :cucumber
task :features => :cucumber do
STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***"
end
rescue LoadError
desc 'cucumber rake task not available (cucumber not installed)'
task :cucumber do
abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin'
end
end
end

View file

@ -0,0 +1,27 @@
require 'rake'
namespace :db do
desc "Dump the current SQLite3 or MySQL database to a sql file"
task :dump_sql do
load 'config/environment.rb'
abcs = ActiveRecord::Base.configurations
case abcs[RAILS_ENV]["adapter"]
when 'mysql'
ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
File.open("db/#{RAILS_ENV}_data.sql", "w+") do |f|
if abcs[RAILS_ENV]["password"].blank?
f << `mysqldump -h #{abcs[RAILS_ENV]["host"]} -u #{abcs[RAILS_ENV]["username"]} #{abcs[RAILS_ENV]["database"]}`
else
f << `mysqldump -h #{abcs[RAILS_ENV]["host"]} -u #{abcs[RAILS_ENV]["username"]} -p#{abcs[RAILS_ENV]["password"]} #{abcs[RAILS_ENV]["database"]}`
end
end
when 'sqlite3'
ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
File.open("db/#{RAILS_ENV}_data.sql", "w+") do |f|
f << `sqlite3 #{abcs[RAILS_ENV]["database"]} .dump`
end
else
raise "Task not supported by '#{abcs[RAILS_ENV]['adapter']}'"
end
end
end

View file

@ -0,0 +1,17 @@
desc ' Create YAML test fixtures from data in an existing database.
Defaults to development database. Set RAILS_ENV to override (taken from Rails Recipes book).'
task :extract_fixtures => :environment do
sql = "SELECT * FROM %s"
skip_tables = ["schema_info", "sessions", "users"]
ActiveRecord::Base.establish_connection
(ActiveRecord::Base.connection.tables - skip_tables).each do |table_name|
i = "000"
File.open("#{RAILS_ROOT}/db/exported_fixtures/#{table_name}.yml", 'w' ) do |file|
data = ActiveRecord::Base.connection.select_all(sql % table_name)
file.write data.inject({}) { |hash, record|
hash["#{table_name}_#{i.succ!}"] = record
hash
}.to_yaml
end
end
end

View file

@ -0,0 +1,34 @@
desc "Copy third-party gems into ./lib"
task :freeze_other_gems do
# TODO Get this list from parsing environment.rb
libraries = %w(redcloth)
require 'rubygems'
require 'find'
libraries.each do |library|
library_gem = Gem.cache.search(library).sort_by { |g| g.version }.last
puts "Freezing #{library} for #{library_gem.version}..."
# TODO Add dependencies to list of libraries to freeze
#library_gem.dependencies.each { |g| libraries << g }
folder_for_library = "#{library_gem.name}-#{library_gem.version}"
system "cd vendor; gem unpack -v '#{library_gem.version}' #{library_gem.name};"
# Copy files recursively to ./lib
folder_for_library_with_lib = "vendor/#{folder_for_library}/lib/"
Find.find(folder_for_library_with_lib) do |original_file|
destination_file = "./lib/" + original_file.gsub(folder_for_library_with_lib, '')
if File.directory?(original_file)
if !File.exist?(destination_file)
Dir.mkdir destination_file
end
else
File.copy original_file, destination_file
end
end
system "rm -r vendor/#{folder_for_library}"
end
end

View file

@ -0,0 +1,8 @@
desc "Load exported fixtures (in db/exported_fixtures) into the current environment's database"
task :load_exported_fixtures => :environment do
require 'active_record/fixtures'
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
Dir.glob(File.join(RAILS_ROOT, 'db', 'exported_fixtures', '*.{yml,csv}')).each do |fixture_file|
Fixtures.create_fixtures('db/exported_fixtures', File.basename(fixture_file, '.*'))
end
end

View file

@ -0,0 +1,50 @@
namespace :query_trace do
desc "Enables the query_trace plugin. Must restart server to take effect."
task :on => :environment do
unless File.exist?("#{RAILS_ROOT}/vendor/query_trace.tar.gz")
Dir.chdir("#{RAILS_ROOT}/vendor") do
url = "https://terralien.devguard.com/svn/projects/plugins/query_trace"
puts "Loading query_trace from #{url}..."
system "svn co #{url} query_trace"
system "tar zcf query_trace.tar.gz --exclude=.svn query_trace"
FileUtils.rm_rf("query_trace")
end
end
Dir.chdir("#{RAILS_ROOT}/vendor/plugins") do
system "tar zxf ../query_trace.tar.gz query_trace"
end
puts "QueryTrace plugin enabled. Must restart server to take effect."
end
desc "Disables the query_trace plugin. Must restart server to take effect."
task :off => :environment do
FileUtils.rm_rf("#{RAILS_ROOT}/vendor/plugins/query_trace")
puts "QueryTrace plugin disabled. Must restart server to take effect."
end
end
namespace :query_analyzer do
desc "Enables the query_analyzer plugin. Must restart server to take effect."
task :on => :environment do
unless File.exist?("#{RAILS_ROOT}/vendor/query_analyzer.tar.gz")
Dir.chdir("#{RAILS_ROOT}/vendor") do
url = "http://svn.nfectio.us/plugins/query_analyzer"
puts "Loading query_analyzer from #{url}..."
system "svn co #{url} query_analyzer"
system "tar zcf query_analyzer.tar.gz --exclude=.svn query_analyzer"
FileUtils.rm_rf("query_analyzer")
end
end
Dir.chdir("#{RAILS_ROOT}/vendor/plugins") do
system "tar zxf ../query_analyzer.tar.gz query_analyzer"
end
puts "QueryAnalyzer plugin enabled. Must restart server to take effect."
end
desc "Disables the query_analyzer plugin. Must restart server to take effect."
task :off => :environment do
FileUtils.rm_rf("#{RAILS_ROOT}/vendor/plugins/query_analyzer")
puts "QueryAnalyzer plugin disabled. Must restart server to take effect."
end
end

View file

@ -0,0 +1,23 @@
namespace :tracks do
desc 'Replace the password of USER with a new one.'
task :password => :environment do
require "highline/import"
user = User.find_by_login(ENV['USER'])
if user.nil?
puts "Sorry, we couldn't find user '#{ENV['USER']}'. To specify a different user, pass USER=username to this task."
exit 0
end
puts "Changing Tracks password for #{ENV['USER']}."
password = ask("New password: ") { |q| q.echo = false }
password_confirmation = ask('Retype new password: ') { |q| q.echo = false }
begin
user.change_password(password, password_confirmation)
rescue ActiveRecord::RecordInvalid
puts "Sorry, we couldn't change #{ENV['USER']}'s password: "
user.errors.each_full { |msg| puts "- #{msg}\n" }
end
end
end

View file

@ -0,0 +1,144 @@
gem 'test-unit', '1.2.3' if RUBY_VERSION.to_f >= 1.9
rspec_gem_dir = nil
Dir["#{RAILS_ROOT}/vendor/gems/*"].each do |subdir|
rspec_gem_dir = subdir if subdir.gsub("#{RAILS_ROOT}/vendor/gems/","") =~ /^(\w+-)?rspec-(\d+)/ && File.exist?("#{subdir}/lib/spec/rake/spectask.rb")
end
rspec_plugin_dir = File.expand_path(File.dirname(__FILE__) + '/../../vendor/plugins/rspec')
if rspec_gem_dir && (test ?d, rspec_plugin_dir)
raise "\n#{'*'*50}\nYou have rspec installed in both vendor/gems and vendor/plugins\nPlease pick one and dispose of the other.\n#{'*'*50}\n\n"
end
if rspec_gem_dir
$LOAD_PATH.unshift("#{rspec_gem_dir}/lib")
elsif File.exist?(rspec_plugin_dir)
$LOAD_PATH.unshift("#{rspec_plugin_dir}/lib")
end
# Don't load rspec if running "rake gems:*"
unless ARGV.any? {|a| a =~ /^gems/}
begin
require 'spec/rake/spectask'
rescue MissingSourceFile
module Spec
module Rake
class SpecTask
def initialize(name)
task name do
# if rspec-rails is a configured gem, this will output helpful material and exit ...
require File.expand_path(File.join(File.dirname(__FILE__),"..","..","config","environment"))
# ... otherwise, do this:
raise <<-MSG
#{"*" * 80}
* You are trying to run an rspec rake task defined in
* #{__FILE__},
* but rspec can not be found in vendor/gems, vendor/plugins or system gems.
#{"*" * 80}
MSG
end
end
end
end
end
end
Rake.application.instance_variable_get('@tasks').delete('default')
spec_prereq = File.exist?(File.join(RAILS_ROOT, 'config', 'database.yml')) ? "db:test:prepare" : :noop
task :noop do
end
task :default => :spec
task :stats => "spec:statsetup"
desc "Run all specs in spec directory (excluding plugin specs)"
Spec::Rake::SpecTask.new(:spec => spec_prereq) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
end
namespace :spec do
desc "Run all specs in spec directory with RCov (excluding plugin specs)"
Spec::Rake::SpecTask.new(:rcov) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
t.rcov = true
t.rcov_opts = lambda do
IO.readlines("#{RAILS_ROOT}/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten
end
end
desc "Print Specdoc for all specs (excluding plugin specs)"
Spec::Rake::SpecTask.new(:doc) do |t|
t.spec_opts = ["--format", "specdoc", "--dry-run"]
t.spec_files = FileList['spec/**/*_spec.rb']
end
desc "Print Specdoc for all plugin examples"
Spec::Rake::SpecTask.new(:plugin_doc) do |t|
t.spec_opts = ["--format", "specdoc", "--dry-run"]
t.spec_files = FileList['vendor/plugins/**/spec/**/*_spec.rb'].exclude('vendor/plugins/rspec/*')
end
[:models, :controllers, :views, :helpers, :lib, :integration].each do |sub|
desc "Run the code examples in spec/#{sub}"
Spec::Rake::SpecTask.new(sub => spec_prereq) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
end
end
desc "Run the code examples in vendor/plugins (except RSpec's own)"
Spec::Rake::SpecTask.new(:plugins => spec_prereq) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['vendor/plugins/**/spec/**/*_spec.rb'].exclude('vendor/plugins/rspec/*').exclude("vendor/plugins/rspec-rails/*")
end
namespace :plugins do
desc "Runs the examples for rspec_on_rails"
Spec::Rake::SpecTask.new(:rspec_on_rails) do |t|
t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""]
t.spec_files = FileList['vendor/plugins/rspec-rails/spec/**/*_spec.rb']
end
end
# Setup specs for stats
task :statsetup do
require 'code_statistics'
::STATS_DIRECTORIES << %w(Model\ specs spec/models) if File.exist?('spec/models')
::STATS_DIRECTORIES << %w(View\ specs spec/views) if File.exist?('spec/views')
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers) if File.exist?('spec/controllers')
::STATS_DIRECTORIES << %w(Helper\ specs spec/helpers) if File.exist?('spec/helpers')
::STATS_DIRECTORIES << %w(Library\ specs spec/lib) if File.exist?('spec/lib')
::STATS_DIRECTORIES << %w(Routing\ specs spec/routing) if File.exist?('spec/routing')
::STATS_DIRECTORIES << %w(Integration\ specs spec/integration) if File.exist?('spec/integration')
::CodeStatistics::TEST_TYPES << "Model specs" if File.exist?('spec/models')
::CodeStatistics::TEST_TYPES << "View specs" if File.exist?('spec/views')
::CodeStatistics::TEST_TYPES << "Controller specs" if File.exist?('spec/controllers')
::CodeStatistics::TEST_TYPES << "Helper specs" if File.exist?('spec/helpers')
::CodeStatistics::TEST_TYPES << "Library specs" if File.exist?('spec/lib')
::CodeStatistics::TEST_TYPES << "Routing specs" if File.exist?('spec/routing')
::CodeStatistics::TEST_TYPES << "Integration specs" if File.exist?('spec/integration')
end
namespace :db do
namespace :fixtures do
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z."
task :load => :environment do
ActiveRecord::Base.establish_connection(Rails.env)
base_dir = File.join(Rails.root, 'spec', 'fixtures')
fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir
require 'active_record/fixtures'
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file|
Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*'))
end
end
end
end
end
end

View file

@ -0,0 +1,15 @@
desc "Initialises the installation, copy the *.tmpl files and directories to versions named without the .tmpl extension. It won't overwrite the files and directories if you've already copied them. You need to manually copy database.yml.tmpl -> database.yml and fill in the details before you run this task."
task :setup_tracks => :environment do
# Check the root directory for template files
FileList["*.tmpl"].each do |template_file|
f = File.basename(template_file) # with suffix
f_only = File.basename(template_file,".tmpl") # without suffix
if File.exists?(f_only)
puts f_only + " already exists"
else
cp_r(f, f_only)
puts f_only + " created"
end
end
end

View file

@ -0,0 +1,40 @@
desc "Updates sqlite/sqlite3 databases created under Tracks 1.03 to the format required for Tracks 1.04. After this is done, you should be able to keep up to date with changes in the schema by running rake db:migrate."
task :upgrade_sqlite_db => :environment do
# Change the three lines below appropriately for your setup
old_db = "tracks_103.db"
new_db = "tracks_104.db"
cmd = "sqlite3"
replace_string = "update todos set done='f' where done=0;\nupdate todos set done='t' where done=1;\nupdate contexts set hide='f' where hide=0;\nupdate contexts set hide='t' where hide=1;\nupdate projects set done='f' where done=0;\nupdate projects set done='t' where done=1;\nCREATE TABLE 'schema_info' (\n 'version' INTEGER default NULL\n);\nINSERT INTO \"schema_info\" VALUES(1);\nCOMMIT;"
# cd to the db directory
cd("db") do
# Dump the old db into the temp file and replace the tinyints with booleans
`#{cmd} #{old_db} .dump | sed "s/tinyint(4) NOT NULL default '0'/boolean default 'f'/" > temp.sql`
# Create a second sqldump file for writing
sqldump = File.open("temp2.sql", "w+")
File.open("temp.sql") do |file|
file.each_line do |line|
# If COMMIT is on the line, insert the replace string
# else just write the line back in
# This effectively replaces COMMIT with the replace string
if /COMMIT/ =~ line
sqldump.write replace_string
else
sqldump.write line
end
end
sqldump.close
end
# Read the second dump back in to a new db
system "#{cmd} #{new_db} < temp2.sql"
puts "Created the a new database called #{new_db}."
# Clean up the temp files
rm("temp.sql")
rm("temp2.sql")
puts "Temporary files cleaned up."
end
# rake db:migrate
puts "Now check the database and run 'rake db:migrate' in the root of your Tracks installation."
end

View file

@ -0,0 +1,27 @@
module Tracks
class Config
def self.salt
SITE_CONFIG['salt']
end
def self.auth_schemes
SITE_CONFIG['authentication_schemes'] || []
end
def self.openid_enabled?
auth_schemes.include?('open_id')
end
def self.cas_enabled?
auth_schemes.include?('cas')
end
def self.prefered_auth?
if SITE_CONFIG['prefered_auth']
SITE_CONFIG['prefered_auth']
else
auth_schemes.first
end
end
end
end

View file

@ -0,0 +1,68 @@
# Inspiration from Bruce Williams [http://codefluency.com/articles/2006/07/01/rails-views-getting-in-context/]
module Tracks
module SourceViewSwitching
class Responder
def initialize(source_view)
@source_view = source_view.underscore.gsub(/\s+/,'_').to_sym rescue nil
end
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?
end
end
module Controller
def self.included(base)
base.send(:helper, Tracks::SourceViewSwitching::Helper)
base.send(:helper_method, :source_view)
end
def source_view_is( s )
s == (params[:_source_view] || @source_view).to_sym
end
def source_view_is_one_of( *s )
s.include?(params[:_source_view].to_sym)
end
def source_view
responder = Tracks::SourceViewSwitching::Responder.new(params[:_source_view] || @source_view)
block_given? ? yield(responder) : responder
end
end
module Helper
def source_view_tag(name)
hidden_field_tag :_source_view, name.underscore.gsub(/\s+/,'_')
end
def source_view_is( s )
s == (params[:_source_view] || @source_view).to_sym
end
def source_view_is_one_of( *s )
s.include?(params[:_source_view].to_sym)
end
end
end
end
ActionController::Base.send(:include, Tracks::SourceViewSwitching::Controller)

View file

@ -0,0 +1,59 @@
module Tracks
module TodoList
# TODO: this module should be deprecated. This could mostly (all?) be replaced by named scopes)
def not_done_todos(opts={})
@not_done_todos ||= self.find_not_done_todos(opts)
end
def done_todos
@done_todos ||= self.find_done_todos
end
def deferred_todos
@deferred_todos ||= self.find_deferred_todos
end
def find_not_done_todos(opts={})
with_not_done_scope(opts) do
self.todos.find(:all, :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC")
end
end
def find_deferred_todos(opts={})
self.todos.find_in_state(:all, :deferred, :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC")
end
def find_done_todos
self.todos.completed.all(:order => "todos.completed_at DESC", :limit => self.user.prefs.show_number_completed)
end
def not_done_todo_count(opts={})
with_not_done_scope(opts) do
self.todos.count
end
end
def with_not_done_scope(opts={})
conditions = ["todos.state = ?", 'active']
if opts.has_key?(:include_project_hidden_todos) && (opts[:include_project_hidden_todos] == true)
conditions = ["(todos.state = ? OR todos.state = ?)", 'active', 'project_hidden']
end
if opts.has_key?(:tag)
conditions = ["todos.state = ? AND taggings.tag_id = ?", 'active', opts[:tag]]
end
self.todos.send :with_scope, :find => {:conditions => conditions, :include => [:taggings]} do
yield
end
end
def done_todo_count
self.todos.count_in_state(:completed)
end
def deferred_todo_count
self.todos.count_in_state(:deferred)
end
end
end

View file

@ -0,0 +1,107 @@
# adapted from https://gist.github.com/471663 and https://rails.lighthouseapp.com/projects/8994/tickets/4690-mongrel-doesnt-work-with-rails-238
def check_mongrel_around_115
begin
# Gem.available? is deprecated from rubygems 1.8.2
Gem::Specification::find_by_name "mongrel", "~>1.1.5"
rescue
if RUBY_VERSION[2] == "9"
false
else
Gem.available?('mongrel', '~>1.1.5')
end
end
end
mongrel115 = check_mongrel_around_115
if Rails.version == '2.3.14' && mongrel115 && self.class.const_defined?(:Mongrel)
# Pulled right from latest rack. Old looked like this in 1.1.0 version.
#
# def [](k)
# super(@names[k] ||= @names[k.downcase])
# end
#
module Rack
module Utils
class HeaderHash < Hash
def [](k)
super(@names[k]) if @names[k]
super(@names[k.downcase])
end
end
end
end
# Code pulled from the ticket above.
#
class Mongrel::CGIWrapper
def header_with_rails_fix(options = 'text/html')
@head['cookie'] = options.delete('cookie').flatten.map { |v| v.sub(/^\n/,'') } if options.class != String and options['cookie']
header_without_rails_fix(options)
end
alias_method_chain :header, :rails_fix
end
# Pulled right from 2.3.10 ActionPack. Simple diff was
#
# if headers.include?('Set-Cookie')
# headers['cookie'] = headers.delete('Set-Cookie').split("\n")
# end
#
# to
#
# if headers['Set-Cookie']
# headers['cookie'] = headers.delete('Set-Cookie').split("\n")
# end
#
module ActionController
class CGIHandler
def self.dispatch_cgi(app, cgi, out = $stdout)
env = cgi.__send__(:env_table)
env.delete "HTTP_CONTENT_LENGTH"
cgi.stdinput.extend ProperStream
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({
"rack.version" => [0,1],
"rack.input" => cgi.stdinput,
"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"] ||= "/"
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
status, headers, body = app.call(env)
begin
out.binmode if out.respond_to?(:binmode)
out.sync = false if out.respond_to?(:sync=)
headers['Status'] = status.to_s
if headers['Set-Cookie']
headers['cookie'] = headers.delete('Set-Cookie').split("\n")
end
out.write(cgi.header(headers))
body.each { |part|
out.write part
out.flush if out.respond_to?(:flush)
}
ensure
body.close if body.respond_to?(:close)
end
end
end
end
end

View file

@ -0,0 +1,21 @@
# Be sure to restart your server when you modify this file.
# These settings change the behavior of Rails 2 apps and will be defaults
# for Rails 3. You can remove this initializer when Rails 3 is released.
if defined?(ActiveRecord)
# Include Active Record class name as root for JSON serialized output.
ActiveRecord::Base.include_root_in_json = true
# Store the full class name (including module namespace) in STI type column.
ActiveRecord::Base.store_full_sti_class = true
end
ActionController::Routing.generate_best_match = false
# Use ISO 8601 format for JSON serialized times and dates.
ActiveSupport.use_standard_json_time_format = true
# Don't escape HTML entities in JSON, leave that for the #json_escape helper.
# if you're including raw json in an HTML page.
ActiveSupport.escape_html_entities_in_json = false

View file

@ -0,0 +1,2 @@
require 'extra_validations'
ActiveRecord::Base.extend ExtraValidations

View file

@ -0,0 +1,29 @@
module ExtraValidations
# Validates the value of the specified attribute by checking for a forbidden string
#
# class Person < ActiveRecord::Base
# validates_does_not_contain :first_name, :string => ','
# end
#
# A string must be provided or else an exception will be raised.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "is invalid")
# * <tt>string</tt> - The string to verify is not included (note: must be supplied!)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_does_not_contain(*attr_names)
configuration = { :message => I18n.translate('activerecord.errors.messages')[:invalid], :on => :save, :string => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
raise(ArgumentError, "A string must be supplied as the :string option of the configuration hash") unless configuration[:string].is_a?(String)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) if value.to_s =~ Regexp.new(Regexp.escape(configuration[:string]))
end
end
end

View file

@ -0,0 +1,35 @@
* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
* Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]
* Add Timeout protection [Rick]
* An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.
* Allow a return_to option to be used instead of the requested url [Josh Peek]
* Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]
* Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]
* Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]
* Allow -'s in #normalize_url [Rick]
* remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]
* Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]
* Just use the path for the return URL, so extra query parameters don't interfere [DHH]
* Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]
* Added normalize_url and applied it to all operations going through the plugin [DHH]
* Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]
* Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]
* Stop relying on root_url being defined, we can just grab the current url instead [DHH]

View file

@ -0,0 +1,231 @@
OpenIdAuthentication
====================
Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
gem install ruby-openid
To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
from that gem.
The specification used is http://openid.net/specs/openid-authentication-2_0.html.
Prerequisites
=============
OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of
database tables to store the authentication keys. So you'll have to run the migration to create these before you get started:
rake open_id_authentication:db:create
Or, use the included generators to install or upgrade:
./script/generate open_id_authentication_tables MigrationName
./script/generate upgrade_open_id_authentication_tables MigrationName
Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
OpenIdAuthentication.store = :file
This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
map.root :controller => 'articles'
This plugin relies on Rails Edge revision 6317 or newer.
Example
=======
This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
not a destination.
Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
model you are using for authentication.
Also of note is the following code block used in the example below:
authenticate_with_open_id do |result, identity_url|
...
end
In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
If you are storing just 'example.com' with your user, the lookup will fail.
There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
OpenIdAuthentication.normalize_url(user.identity_url)
The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
Use the above code in your User model and validate OpenID URLs before saving them.
config/routes.rb
map.root :controller => 'articles'
map.resource :session
app/views/sessions/new.erb
<% form_tag(session_url) do %>
<p>
<label for="name">Username:</label>
<%= text_field_tag "name" %>
</p>
<p>
<label for="password">Password:</label>
<%= password_field_tag %>
</p>
<p>
...or use:
</p>
<p>
<label for="openid_identifier">OpenID:</label>
<%= text_field_tag "openid_identifier" %>
</p>
<p>
<%= submit_tag 'Sign in', :disable_with => "Signing in&hellip;" %>
</p>
<% end %>
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
if using_open_id?
open_id_authentication
else
password_authentication(params[:name], params[:password])
end
end
protected
def password_authentication(name, password)
if @current_user = @account.users.authenticate(params[:name], params[:password])
successful_login
else
failed_login "Sorry, that username/password doesn't work"
end
end
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful?
if @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end
private
def successful_login
session[:user_id] = @current_user.id
redirect_to(root_url)
end
def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end
If you're fine with the result messages above and don't need individual logic on a per-failure basis,
you can collapse the case into a mere boolean:
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
end
end
end
Simple Registration OpenID Extension
====================================
Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
You can support it in your app by changing #open_id_authentication
def open_id_authentication(identity_url)
# Pass optional :required and :optional keys to specify what sreg fields you want.
# Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
authenticate_with_open_id(identity_url,
:required => [ :nickname, :email ],
:optional => :fullname) do |result, identity_url, registration|
case result.status
when :missing
failed_login "Sorry, the OpenID server couldn't be found"
when :invalid
failed_login "Sorry, but this does not appear to be a valid OpenID"
when :canceled
failed_login "OpenID verification was canceled"
when :failed
failed_login "Sorry, the OpenID verification failed"
when :successful
if @current_user = @account.users.find_by_identity_url(identity_url)
assign_registration_attributes!(registration)
if current_user.save
successful_login
else
failed_login "Your OpenID profile registration failed: " +
@current_user.errors.full_messages.to_sentence
end
else
failed_login "Sorry, no user by that identity URL exists"
end
end
end
end
# registration is a hash containing the valid sreg keys given above
# use this to map them to fields of your user model
def assign_registration_attributes!(registration)
model_to_registration_mapping.each do |model_attribute, registration_attribute|
unless registration[registration_attribute].blank?
@current_user.send("#{model_attribute}=", registration[registration_attribute])
end
end
end
def model_to_registration_mapping
{ :login => 'nickname', :email => 'email', :display_name => 'fullname' }
end
Attribute Exchange OpenID Extension
===================================
Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
authenticate_with_open_id(identity_url,
:required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the open_id_authentication plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the open_id_authentication plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'OpenIdAuthentication'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,11 @@
class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end

View file

@ -0,0 +1,20 @@
class <%= class_name %> < ActiveRecord::Migration
def self.up
create_table :open_id_authentication_associations, :force => true do |t|
t.integer :issued, :lifetime
t.string :handle, :assoc_type
t.binary :server_url, :secret
end
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end
end
def self.down
drop_table :open_id_authentication_associations
drop_table :open_id_authentication_nonces
end
end

View file

@ -0,0 +1,26 @@
class <%= class_name %> < ActiveRecord::Migration
def self.up
drop_table :open_id_authentication_settings
drop_table :open_id_authentication_nonces
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end
end
def self.down
drop_table :open_id_authentication_nonces
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :created
t.string :nonce
end
create_table :open_id_authentication_settings, :force => true do |t|
t.string :setting
t.binary :value
end
end
end

View file

@ -0,0 +1,11 @@
class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end

View file

@ -0,0 +1,18 @@
if config.respond_to?(:gems)
config.gem 'ruby-openid', :lib => 'openid', :version => '>=2.0.4'
else
begin
require 'openid'
rescue LoadError
begin
gem 'ruby-openid', '>=2.0.4'
rescue Gem::LoadError
puts "Install the ruby-openid gem to enable OpenID support"
end
end
end
config.to_prepare do
OpenID::Util.logger = Rails.logger
ActionController::Base.send :include, OpenIdAuthentication
end

View file

@ -0,0 +1,240 @@
require 'uri'
require 'openid/extensions/sreg'
require 'openid/extensions/ax'
require 'openid/store/filesystem'
require File.dirname(__FILE__) + '/open_id_authentication/association'
require File.dirname(__FILE__) + '/open_id_authentication/nonce'
require File.dirname(__FILE__) + '/open_id_authentication/db_store'
require File.dirname(__FILE__) + '/open_id_authentication/request'
require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
module OpenIdAuthentication
OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
def self.store
@@store
end
def self.store=(*store_option)
store, *parameters = *([ store_option ].flatten)
@@store = case store
when :db
OpenIdAuthentication::DbStore.new
when :file
OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
else
store
end
end
self.store = :db
class InvalidOpenId < StandardError
end
class Result
ERROR_MESSAGES = {
:missing => "Sorry, the OpenID server couldn't be found",
:invalid => "Sorry, but this does not appear to be a valid OpenID",
:canceled => "OpenID verification was canceled",
:failed => "OpenID verification failed",
:setup_needed => "OpenID verification needs setup"
}
def self.[](code)
new(code)
end
def initialize(code)
@code = code
end
def status
@code
end
ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
def successful?
@code == :successful
end
def unsuccessful?
ERROR_MESSAGES.keys.include?(@code)
end
def message
ERROR_MESSAGES[@code]
end
end
# normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
def self.normalize_identifier(identifier)
# clean up whitespace
identifier = identifier.to_s.strip
# if an XRI has a prefix, strip it.
identifier.gsub!(/xri:\/\//i, '')
# dodge XRIs -- TODO: validate, don't just skip.
unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
# does it begin with http? if not, add it.
identifier = "http://#{identifier}" unless identifier =~ /^http/i
# strip any fragments
identifier.gsub!(/\#(.*)$/, '')
begin
uri = URI.parse(identifier)
uri.scheme = uri.scheme.downcase # URI should do this
identifier = uri.normalize.to_s
rescue URI::InvalidURIError
raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
end
end
return identifier
end
# deprecated for OpenID 2.0, where not all OpenIDs are URLs
def self.normalize_url(url)
ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
self.normalize_identifier(url)
end
protected
def normalize_url(url)
OpenIdAuthentication.normalize_url(url)
end
def normalize_identifier(url)
OpenIdAuthentication.normalize_identifier(url)
end
# The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
# because that's what the specification dictates in order to get browser auto-complete working across sites
def using_open_id?(identity_url = nil) #:doc:
identity_url ||= params[:openid_identifier] || params[:openid_url]
!identity_url.blank? || params[:open_id_complete]
end
def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
identity_url ||= params[:openid_identifier] || params[:openid_url]
if params[:open_id_complete].nil?
begin_open_id_authentication(identity_url, options, &block)
else
complete_open_id_authentication(&block)
end
end
private
def begin_open_id_authentication(identity_url, options = {})
identity_url = normalize_identifier(identity_url)
return_to = options.delete(:return_to)
method = options.delete(:method)
options[:required] ||= [] # reduces validation later
options[:optional] ||= []
open_id_request = open_id_consumer.begin(identity_url)
add_simple_registration_fields(open_id_request, options)
add_ax_fields(open_id_request, options)
redirect_to(open_id_redirect_url(open_id_request, return_to, method))
rescue OpenIdAuthentication::InvalidOpenId => e
yield Result[:invalid], identity_url, nil
rescue OpenID::OpenIDError, Timeout::Error => e
logger.error("[OPENID] #{e}")
yield Result[:missing], identity_url, nil
end
def complete_open_id_authentication
params_with_path = params.reject { |key, value| request.path_parameters[key] }
params_with_path.delete(:format)
open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
case open_id_response.status
when OpenID::Consumer::SUCCESS
profile_data = {}
# merge the SReg data and the AX data into a single hash of profile data
[ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
if data_response.from_success_response( open_id_response )
profile_data.merge! data_response.from_success_response( open_id_response ).data
end
end
yield Result[:successful], identity_url, profile_data
when OpenID::Consumer::CANCEL
yield Result[:canceled], identity_url, nil
when OpenID::Consumer::FAILURE
yield Result[:failed], identity_url, nil
when OpenID::Consumer::SETUP_NEEDED
yield Result[:setup_needed], open_id_response.setup_url, nil
end
end
def open_id_consumer
OpenID::Consumer.new(session, OpenIdAuthentication.store)
end
def add_simple_registration_fields(open_id_request, fields)
sreg_request = OpenID::SReg::Request.new
# filter out AX identifiers (URIs)
required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
sreg_request.request_fields(required_fields, true) unless required_fields.blank?
sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
open_id_request.add_extension(sreg_request)
end
def add_ax_fields( open_id_request, fields )
ax_request = OpenID::AX::FetchRequest.new
# look through the :required and :optional fields for URIs (AX identifiers)
fields[:required].each do |f|
next unless f =~ /^https?:\/\//
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
end
fields[:optional].each do |f|
next unless f =~ /^https?:\/\//
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
end
open_id_request.add_extension( ax_request )
end
def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
open_id_request.return_to_args['_method'] = (method || request.method).to_s
open_id_request.return_to_args['open_id_complete'] = '1'
open_id_request.redirect_url(root_url, return_to || requested_url)
end
def requested_url
relative_url_root = self.class.respond_to?(:relative_url_root) ?
self.class.relative_url_root.to_s :
request.relative_url_root
"#{request.protocol}#{request.host_with_port}#{ActionController::Base.relative_url_root}#{request.path}"
end
def timeout_protection_from_identity_server
yield
rescue Timeout::Error
Class.new do
def status
OpenID::FAILURE
end
def msg
"Identity server timed out"
end
end.new
end
end

View file

@ -0,0 +1,9 @@
module OpenIdAuthentication
class Association < ActiveRecord::Base
set_table_name :open_id_authentication_associations
def from_record
OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
end
end
end

View file

@ -0,0 +1,55 @@
require 'openid/store/interface'
module OpenIdAuthentication
class DbStore < OpenID::Store::Interface
def self.cleanup_nonces
now = Time.now.to_i
Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
end
def self.cleanup_associations
now = Time.now.to_i
Association.delete_all(['issued + lifetime > ?',now])
end
def store_association(server_url, assoc)
remove_association(server_url, assoc.handle)
Association.create(:server_url => server_url,
:handle => assoc.handle,
:secret => assoc.secret,
:issued => assoc.issued,
:lifetime => assoc.lifetime,
:assoc_type => assoc.assoc_type)
end
def get_association(server_url, handle = nil)
assocs = if handle.blank?
Association.find_all_by_server_url(server_url)
else
Association.find_all_by_server_url_and_handle(server_url, handle)
end
assocs.reverse.each do |assoc|
a = assoc.from_record
if a.expires_in == 0
assoc.destroy
else
return a
end
end if assocs.any?
return nil
end
def remove_association(server_url, handle)
Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
end
def use_nonce(server_url, timestamp, salt)
return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt)
return true
end
end
end

View file

@ -0,0 +1,5 @@
module OpenIdAuthentication
class Nonce < ActiveRecord::Base
set_table_name :open_id_authentication_nonces
end
end

View file

@ -0,0 +1,23 @@
module OpenIdAuthentication
module Request
def self.included(base)
base.alias_method_chain :request_method, :openid
end
def request_method_with_openid
if !parameters[:_method].blank? && parameters[:open_id_complete] == '1'
parameters[:_method].to_sym
else
request_method_without_openid
end
end
end
end
# In Rails 2.3, the request object has been renamed
# from AbstractRequest to Request
if defined? ActionController::Request
ActionController::Request.send :include, OpenIdAuthentication::Request
else
ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request
end

View file

@ -0,0 +1,20 @@
# http://trac.openidenabled.com/trac/ticket/156
module OpenID
@@timeout_threshold = 20
def self.timeout_threshold
@@timeout_threshold
end
def self.timeout_threshold=(value)
@@timeout_threshold = value
end
class StandardFetcher
def make_http(uri)
http = @proxy.new(uri.host, uri.port)
http.read_timeout = http.open_timeout = OpenID.timeout_threshold
http
end
end
end

View file

@ -0,0 +1,30 @@
namespace :open_id_authentication do
namespace :db do
desc "Creates authentication tables for use with OpenIdAuthentication"
task :create => :environment do
generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"])
end
desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x"
task :upgrade => :environment do
generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"])
end
def generate_migration(args)
require 'rails_generator'
require 'rails_generator/scripts/generate'
if ActiveRecord::Base.connection.supports_migrations?
Rails::Generator::Scripts::Generate.new.run(args)
else
raise "Task unavailable to this database (no migration support)"
end
end
desc "Clear the authentication tables"
task :clear => :environment do
OpenIdAuthentication::DbStore.cleanup_nonces
OpenIdAuthentication::DbStore.cleanup_associations
end
end
end

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '/test_helper'
class NormalizeTest < Test::Unit::TestCase
include OpenIdAuthentication
NORMALIZATIONS = {
"openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
"http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
"https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler",
"HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER",
"HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER",
"loudthinking.com" => "http://loudthinking.com/",
"http://loudthinking.com" => "http://loudthinking.com/",
"http://loudthinking.com:80" => "http://loudthinking.com/",
"https://loudthinking.com:443" => "https://loudthinking.com/",
"http://loudthinking.com:8080" => "http://loudthinking.com:8080/",
"techno-weenie.net" => "http://techno-weenie.net/",
"http://techno-weenie.net" => "http://techno-weenie.net/",
"http://techno-weenie.net " => "http://techno-weenie.net/",
"=name" => "=name"
}
def test_normalizations
NORMALIZATIONS.each do |from, to|
assert_equal to, normalize_identifier(from)
end
end
def test_broken_open_id
assert_raises(InvalidOpenId) { normalize_identifier(nil) }
end
end

View file

@ -0,0 +1,46 @@
require File.dirname(__FILE__) + '/test_helper'
class OpenIdAuthenticationTest < Test::Unit::TestCase
def setup
@controller = Class.new do
include OpenIdAuthentication
def params() {} end
end.new
end
def test_authentication_should_fail_when_the_identity_server_is_missing
open_id_consumer = mock()
open_id_consumer.expects(:begin).raises(OpenID::OpenIDError)
@controller.expects(:open_id_consumer).returns(open_id_consumer)
@controller.expects(:logger).returns(mock(:error => true))
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
assert result.missing?
assert_equal "Sorry, the OpenID server couldn't be found", result.message
end
end
def test_authentication_should_be_invalid_when_the_identity_url_is_invalid
@controller.send(:authenticate_with_open_id, "!") do |result, identity_url|
assert result.invalid?, "Result expected to be invalid but was not"
assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message
end
end
def test_authentication_should_fail_when_the_identity_server_times_out
open_id_consumer = mock()
open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.")
@controller.expects(:open_id_consumer).returns(open_id_consumer)
@controller.expects(:logger).returns(mock(:error => true))
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
assert result.missing?
assert_equal "Sorry, the OpenID server couldn't be found", result.message
end
end
def test_authentication_should_begin_when_the_identity_server_is_present
@controller.expects(:begin_open_id_authentication)
@controller.send(:authenticate_with_open_id, "http://someone.example.com")
end
end

View file

@ -0,0 +1,14 @@
require File.dirname(__FILE__) + '/test_helper'
class StatusTest < Test::Unit::TestCase
include OpenIdAuthentication
def test_state_conditional
assert Result[:missing].missing?
assert Result[:missing].unsuccessful?
assert !Result[:missing].successful?
assert Result[:successful].successful?
assert !Result[:successful].unsuccessful?
end
end

View file

@ -0,0 +1,17 @@
require 'test/unit'
require 'rubygems'
gem 'activesupport'
require 'active_support'
gem 'actionpack'
require 'action_controller'
gem 'mocha'
require 'mocha'
gem 'ruby-openid'
require 'openid'
RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT
require File.dirname(__FILE__) + "/../lib/open_id_authentication"

View file

@ -0,0 +1,7 @@
ResourceFeeder
==============
Simple feeds for resources
NOTE: This plugin depends on the latest version of simply_helpful, available here:
http://dev.rubyonrails.org/svn/rails/plugins/simply_helpful/

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the resource_feed plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the resource_feed plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ResourceFeed'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,2 @@
require 'resource_feeder'
ActionController::Base.send(:include, ResourceFeeder::Rss, ResourceFeeder::Atom)

View file

@ -0,0 +1,2 @@
require 'resource_feeder/rss'
require 'resource_feeder/atom'

View file

@ -0,0 +1,67 @@
require 'resource_feeder/common'
module ResourceFeeder
module Atom
include ResourceFeeder::Common
include ActionController::Routing
extend self
def render_atom_feed_for(resources, options = {})
render :text => atom_feed_for(resources, options), :content_type => Mime::ATOM
end
def atom_feed_for(resources, options = {})
xml = Builder::XmlMarkup.new(:indent => 2)
options[:feed] ||= {}
options[:item] ||= {}
options[:url_writer] ||= self
if options[:class] || resources.first
klass = options[:class] || resources.first.class
new_record = klass.new
else
options[:feed] = { :title => "Empty", :link => "http://example.com" }
end
options[:feed][:title] ||= klass.name.pluralize
options[:feed][:id] ||= "tag:#{request.host_with_port}:#{klass.name.pluralize}"
options[:feed][:link] ||= polymorphic_url(new_record, :controller => options[:url_writer].controller_name)
options[:item][:title] ||= [ :title, :subject, :headline, :name ]
options[:item][:description] ||= [ :description, :body, :content ]
options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ]
options[:item][:author] ||= [ :author, :creator ]
resource_link = lambda { |r| polymorphic_url(r, :controller => options[:url_writer].controller_name) }
xml.instruct!
xml.feed "xml:lang" => "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do
xml.title(options[:feed][:title])
xml.id(options[:feed][:id])
xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:feed][:link])
xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:feed][:self]) if options[:feed][:self]
xml.subtitle(options[:feed][:description]) if options[:feed][:description]
for resource in resources
published_at = call_or_read(options[:item][:pub_date], resource)
xml.entry do
xml.title(call_or_read(options[:item][:title], resource))
xml.content(call_or_read(options[:item][:description], resource), :type => 'html')
xml.id("tag:#{request.host_with_port},#{published_at.xmlschema}:#{call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource)}")
xml.published(published_at.xmlschema)
xml.updated((resource.respond_to?(:updated_at) ? call_or_read(options[:item][:pub_date] || :updated_at, resource) : published_at).xmlschema)
xml.link(:rel => 'alternate', :type => 'text/html', :href => call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource))
if author = call_or_read(options[:item][:author], resource)
xml.author do
xml.name()
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
module ResourceFeeder
module Common
private
def call_or_read(procedure_or_attributes, resource)
case procedure_or_attributes
when nil
raise ArgumentError, "WTF is nil here? #{resource.inspect}"
when Array
attributes = procedure_or_attributes
if attr = attributes.select { |a| resource.respond_to?(a) }.first
resource.send attr
end
when Symbol
attribute = procedure_or_attributes
resource.send(attribute)
when Proc
procedure = procedure_or_attributes
procedure.call(resource)
else
raise ArgumentError, "WTF is #{procedure_or_attributes.inspect} here? #{resource.inspect}"
end
end
end
end

View file

@ -0,0 +1,68 @@
require 'resource_feeder/common'
module ResourceFeeder
module Rss
include ResourceFeeder::Common
include ActionController::Routing
extend self
def render_rss_feed_for(resources, options = {})
render :text => rss_feed_for(resources, options), :content_type => Mime::RSS
end
def rss_feed_for(resources, options = {})
xml = Builder::XmlMarkup.new(:indent => 2)
options[:feed] ||= {}
options[:item] ||= {}
options[:url_writer] ||= self
if options[:class] || resources.first
klass = options[:class] || resources.first.class
new_record = klass.new
else
options[:feed] = { :title => "Empty", :link => "http://example.com" }
end
use_content_encoded = options[:item].has_key?(:content_encoded)
options[:feed][:title] ||= klass.name.pluralize
options[:feed][:link] ||= polymorphic_url(new_record, :controller => options[:url_writer].controller_name)
options[:feed][:language] ||= "en-us"
options[:feed][:ttl] ||= "40"
options[:item][:title] ||= [ :title, :subject, :headline, :name ]
options[:item][:description] ||= [ :description, :body, :content ]
options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ]
resource_link = lambda { |r| polymorphic_url(r, :controller => options[:url_writer].controller_name) }
rss_root_attributes = { :version => 2.0 }
rss_root_attributes.merge!("xmlns:content" => "http://purl.org/rss/1.0/modules/content/") if use_content_encoded
xml.instruct!
xml.rss(rss_root_attributes) do
xml.channel do
xml.title(options[:feed][:title])
xml.link(options[:feed][:link])
xml.description(options[:feed][:description]) if options[:feed][:description]
xml.language(options[:feed][:language])
xml.ttl(options[:feed][:ttl])
for resource in resources
xml.item do
xml.title(call_or_read(options[:item][:title], resource))
xml.description(call_or_read(options[:item][:description], resource))
if use_content_encoded then
xml.content(:encoded) { xml.cdata!(call_or_read(options[:item][:content_encoded], resource)) }
end
xml.pubDate(call_or_read(options[:item][:pub_date], resource).to_s(:rfc822))
xml.guid(call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource))
xml.link(call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource))
end
end
end
end
end
end
end

View file

@ -0,0 +1,85 @@
require File.dirname(__FILE__) + '/test_helper'
class AtomFeedTest < Test::Unit::TestCase
attr_reader :request
def setup
@request = OpenStruct.new
@request.host_with_port = 'example.com'
@records = Array.new(5).fill(Post.new)
@records.each &:save
end
def test_default_atom_feed
atom_feed_for @records
assert_select 'feed' do
assert_select '>title', 'Posts'
assert_select '>id', "tag:#{request.host_with_port}:Posts"
assert_select '>link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='http://example.com/posts']"
end
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (title)'
assert_select "content[type='html']", '&lt;p&gt;feed description (description)&lt;/p&gt;'
assert_select 'id', "tag:#{request.host_with_port},#{@records.first.created_at.xmlschema}:#{'http://example.com/posts/1'}"
assert_select 'published', @records.first.created_at.xmlschema
assert_select 'updated', @records.first.created_at.xmlschema
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='http://example.com/posts/1']"
end
end
end
end
def test_should_allow_custom_feed_options
atom_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :self => '/posts.atom' }
assert_select 'feed>title', 'Custom Posts'
assert_select "feed>link[href='/posts']"
assert_select 'feed>subtitle', 'stuff'
assert_select 'feed>link' do
assert_select "[rel='self']"
assert_select "[type='application/atom+xml']"
assert_select "[href='/posts.atom']"
end
end
def test_should_allow_custom_item_attributes
atom_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id }
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select "content[type='html']", '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'id', "tag:#{request.host_with_port},#{(@records.first.created_at - 5.minutes).xmlschema}:1"
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='1']"
end
end
end
def test_should_allow_custom_item_attribute_blocks
atom_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date },
:link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } }
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select "content[type='html']", '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'id', /:\d+$/
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href=?]", /^\/\d+$/
end
end
end
end

View file

@ -0,0 +1,86 @@
require File.dirname(__FILE__) + '/test_helper'
class RssFeedTest < Test::Unit::TestCase
def setup
@records = Array.new(5).fill(Post.new)
@records.each &:save
end
def test_default_rss_feed
rss_feed_for @records
assert_select 'rss[version="2.0"]' do
assert_select 'channel' do
assert_select '>title', 'Posts'
assert_select '>link', 'http://example.com/posts'
assert_select 'language', 'en-us'
assert_select 'ttl', '40'
end
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (title)'
assert_select 'description', '&lt;p&gt;feed description (description)&lt;/p&gt;'
%w(guid link).each do |node|
assert_select node, 'http://example.com/posts/1'
end
assert_select 'pubDate', @records.first.created_at.to_s(:rfc822)
end
end
end
def test_should_allow_custom_feed_options
rss_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :language => 'en-gb', :ttl => '80' }
assert_select 'channel>title', 'Custom Posts'
assert_select 'channel>link', '/posts'
assert_select 'channel>description', 'stuff'
assert_select 'channel>language', 'en-gb'
assert_select 'channel>ttl', '80'
end
def test_should_allow_custom_item_attributes
rss_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id }
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select 'description', '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822)
assert_select 'link', '1'
assert_select 'guid', '1'
end
end
def test_should_allow_custom_item_attribute_blocks
rss_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date },
:link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } }
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select 'description', '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822)
end
end
# note that assert_select isnt easily able to get elements that have xml namespaces (as it thinks they are
# invalid html psuedo children), so we do some manual testing with the response body
def test_should_allow_content_encoded_for_items
rss_feed_for @records, :item => { :content_encoded => :full_html_body }
html_content = "<strong>Here is some <i>full</i> content, with out any excerpts</strong>"
assert_equal 5, @response.body.scan("<![CDATA[#{html_content}]]>").size
assert_select 'item', 5 do
assert_select 'description + *', "<![CDATA[#{html_content}" # assert_select seems to strip the ending cdata tag
end
end
def test_should_have_content_encoded_namespace_if_used
rss_feed_for @records, :item => { :content_encoded => :full_html_body }
assert_equal %[<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">\n],
@response.body.grep(/<rss version="2\.0.*"/).first
end
def test_should_have_normal_rss_root_without_content_encoded
rss_feed_for @records
assert_equal %[<rss version="2.0">\n],
@response.body.grep(/<rss version="2\.0.*"/).first
end
end

View file

@ -0,0 +1,64 @@
RAILS_ENV = 'test'
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
require 'action_controller/test_process'
require 'breakpoint'
require 'ostruct'
class Post
attr_reader :id, :created_at
def save; @id = 1; @created_at = Time.now.utc end
def new_record?; @id.nil? end
[:title, :name].each do |attr_name|
define_method attr_name do
"feed title (#{attr_name})"
end
end
[:description, :body].each do |attr_name|
define_method attr_name do
"<p>feed description (#{attr_name})</p>"
end
end
def full_html_body
"<strong>Here is some <i>full</i> content, with out any excerpts</strong>"
end
def create_date
@created_at - 5.minutes
end
end
class Test::Unit::TestCase
include ResourceFeeder::Rss, ResourceFeeder::Atom
def render_feed(xml)
@response = OpenStruct.new
@response.headers = {'Content-Type' => 'text/xml'}
@response.body = xml
end
def rss_feed_for_with_ostruct(resources, options = {})
render_feed rss_feed_for_without_ostruct(resources, options)
end
def atom_feed_for_with_ostruct(resources, options = {})
render_feed atom_feed_for_without_ostruct(resources, options)
end
alias_method_chain :rss_feed_for, :ostruct
alias_method_chain :atom_feed_for, :ostruct
def html_document
@html_document ||= HTML::Document.new(@response.body, false, true)
end
def posts_url
"http://example.com/posts"
end
def post_url(post)
"http://example.com/posts/#{post.id}"
end
end

View file

@ -0,0 +1,5 @@
SimpleLdapAuthenticator
=======================
Allows for simple authentication to an LDAP server with a minimum of
configuration. See the RDoc for details.

View file

@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the simple_ldap_authenticator plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the simple_ldap_authenticator plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'SimpleLdapAuthenticator'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,2 @@
# Include hook code here
#require 'simple_ldap_authenticator'

View file

@ -0,0 +1 @@
# Install hook code here

View file

@ -0,0 +1,127 @@
# SimpleLdapAuthenticator
#
# This plugin supports both Ruby/LDAP and Net::LDAP, defaulting to Ruby/LDAP
# if it is available. If both are installed and you want to force the use of
# Net::LDAP, set SimpleLdapAuthenticator.ldap_library = 'net/ldap'.
# Allows for easily authenticating users via LDAP (or LDAPS). If authenticating
# via LDAP to a server running on localhost, you should only have to configure
# the login_format.
#
# Can be configured using the following accessors (with examples):
# * login_format = '%s@domain.com' # Active Directory, OR
# * login_format = 'cn=%s,cn=users,o=organization,c=us' # Other LDAP servers
# * servers = ['dc1.domain.com', 'dc2.domain.com'] # names/addresses of LDAP servers to use
# * use_ssl = true # for logging in via LDAPS
# * port = 3289 # instead of 389 for LDAP or 636 for LDAPS
# * logger = RAILS_DEFAULT_LOGGER # for logging authentication successes/failures
#
# The class is used as a global variable, you are not supposed to create an
# instance of it. For example:
#
# require 'simple_ldap_authenticator'
# SimpleLdapAuthenticator.servers = %w'dc1.domain.com dc2.domain.com'
# SimpleLdapAuthenticator.use_ssl = true
# SimpleLdapAuthenticator.login_format = '%s@domain.com'
# SimpleLdapAuthenticator.logger = RAILS_DEFAULT_LOGGER
# class LoginController < ApplicationController
# def login
# return redirect_to(:action=>'try_again') unless SimpleLdapAuthenticator.valid?(params[:username], params[:password])
# session[:username] = params[:username]
# end
# end
class SimpleLdapAuthenticator
class << self
@servers = ['127.0.0.1']
@use_ssl = false
@login_format = '%s'
attr_accessor :servers, :use_ssl, :port, :login_format, :logger, :connection, :ldap_library
# Load the required LDAP library, either 'ldap' or 'net/ldap'
def load_ldap_library
return if @ldap_library_loaded
if ldap_library
if ldap_library == 'net/ldap'
require 'net/ldap'
else
require 'ldap'
require 'ldap/control'
end
else
begin
require 'ldap'
require 'ldap/control'
ldap_library = 'ldap'
rescue LoadError
require 'net/ldap'
ldap_library = 'net/ldap'
end
end
@ldap_library_loaded = true
end
# The next LDAP server to which to connect
def server
servers[0]
end
# The connection to the LDAP server. A single connection is made and the
# connection is only changed if a server returns an error other than
# invalid password.
def connection
return @connection if @connection
load_ldap_library
@connection = if ldap_library == 'net/ldap'
Net::LDAP.new(:host=>server, :port=>(port), :encryption=>(:simple_tls if use_ssl))
else
(use_ssl ? LDAP::SSLConn : LDAP::Conn).new(server, port)
end
end
# The port to use. Defaults to 389 for LDAP and 636 for LDAPS.
def port
@port ||= use_ssl ? 636 : 389
end
# Disconnect from current LDAP server and use a different LDAP server on the
# next authentication attempt
def switch_server
self.connection = nil
servers << servers.shift
end
# Check the validity of a login/password combination
def valid?(login, password)
if ldap_library == 'net/ldap'
connection.authenticate(login_format % login.to_s, password.to_s)
begin
if connection.bind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
else
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{connection.get_operation_result.code} #{connection.get_operation_result.message}") if logger
switch_server unless connection.get_operation_result.code == 49
false
end
rescue Net::LDAP::LdapError => error
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server
false
end
else
connection.unbind if connection.bound?
begin
connection.bind(login_format % login.to_s, password.to_s)
connection.unbind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
rescue LDAP::ResultError => error
connection.unbind if connection.bound?
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server unless error.message == 'Invalid credentials'
false
end
end
end
end
end

View file

@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :simple_ldap_authenticator do
# # Task goes here
# end

View file

@ -0,0 +1,8 @@
require 'test/unit'
class SimpleLdapAuthenticatorTest < Test::Unit::TestCase
# Replace this with your real tests.
def test_this_plugin
flunk
end
end

View file

@ -0,0 +1,2 @@
.DS_Store
doc

View file

@ -0,0 +1,270 @@
= Skinny Spec
Skinny Spec is a collection of spec helper methods designed to help trim the fat and DRY up
some of the bloat that sometimes results from properly specing your classes and templates.
== Requirements and Recommendations
Obviously you'll need to be using RSpec[http://github.com/dchelimsky/rspec/tree/master] and
Rspec-Rails[http://github.com/dchelimsky/rspec-rails/tree/master] as your testing framework.
Skinny Spec was originally designed [and best enjoyed] if you're using
Haml[http://github.com/nex3/haml/tree/master] and
make_resourceful[http://github.com/rsl/make_resourceful/tree/master] but will default to
ERb and a facsimile of Rails' default scaffolding [for the views and controllers, respectively]
if Haml and/or make_resourceful are not available. I recommend using them though. :)
In addition, Skinny Spec uses Ruby2Ruby to make nicer expectation messages and you'll want to
have that installed as well. It's not a dependency or anything but it <i>is</i> highly
recommended.
== Setup
Once you've installed the plugin in your app's vendor/plugins folder, you're ready to rock!
Skinny Spec includes itself into the proper RSpec classes so there's no configuration on your
part. Sweet!
== Usage
The simplest way to use Skinny Specs is to generate a resource scaffold:
script/generate skinny_scaffold User
This command takes the usual complement of attribute definitions like
<tt>script/generate scaffold</tt>. Then have a look at the generated files (particularly the
specs) to see what's new and different with Skinny Spec.
=== Controller Specs
Let's look at the controller specs.
describe UsersController do
describe "GET :index" do
before(:each) do
@users = stub_index(User)
end
it_should_find_and_assign :users
it_should_render :template, "index"
end
# ...
describe "POST :create" do
describe "when successful" do
before(:each) do
@user = stub_create(User)
end
it_should_initialize_and_save :user
it_should_redirect_to { user_url(@user) }
end
# ...
First thing you should see is an example group for <tt>GET :index</tt>. That <tt>stub_index</tt> method there
does a lot of work behind the curtain. I'll leave it up to you to check the documentation for it
(and its brothers and sister methods like <tt>stub_new</tt>) but I will point out that the
methods named <tt>stub_<i>controller_method</i></tt> should only be used for stubbing and
mocking the main object of the method. To create mocks for other ancillary objects, please
use <tt>stub_find_all</tt>, <tt>stub_find_one</tt>, and <tt>stub_initialize</tt>. The reason
for this is because the former methods actually save us a step by defining an implicit
controller method request. If you add a new method to your resource routing, you'll want to
use the helper method <tt>define_request</tt> in those example groups to define an explicit
request, like so:
describe "PUT :demote" do
define_request { put :demote }
# ...
end
You can also define a method called <tt>shared_request</tt> to "share" a
<tt>define_request</tt> across nested describe blocks, like so:
describe "POST :create" do
def shared_request
post :create
end
describe "when successful" do
# ...
end
describe "when unsuccessful" do
# ...
end
end
Note: When you're adding longer, more complicated controller specs you can still leverage
implicit and explicit requests by calling <tt>do_request</tt> in your spec as in the following
example:
# Note this controller is UsersController and _not_ CategoriesController
# and that loading the categories isn't part of the default actions
# and cannot use the <tt>stub_<i>controller_method</i></tt> helpers
# [which create implicit requests based on the controller method in the name]
# but uses <tt>stub_find_all</tt> instead
describe "GET :new" do
before(:each) do
@user = stub_new(User)
@categories = stub_find_all(Category)
end
# ...
it "should preload categories" do
Category.should_receive(:find).with(:all)
do_request
end
it "should assign @categories" do
do_request
assigns[:categories].should == @categories
end
end
Finally we get to the meat of the spec and of Skinny Specs itself: the actual expectations.
The first thing you'll notice is the use of example group (read: "describe" block) level methods
instead of the usual example (read: "it") blocks. Using this helper at the example group level
saves us three lines over using an example block. (If this isn't significant to you, this is
probably the wrong plugin for you as well. Sorry.) Note that none of these methods use the
instance variables defined in the "before" block because they are all nil at the example block
level. Let's look at a sample method to see how it works:
it_should_find_and_assign :users
This actually wraps two different expectations: one that <tt>User.should_receive(:find).with(:all)</tt>
and another that the instance variable <tt>@users</tt> is assigned with the return value from that finder call.
If you need to add more detailed arguments to the find, you can easily break this into two different
expectations like:
it_should_find :users, :limit => 2
it_should_assign :users
See the documentation for the <tt>it_should_find</tt> for more information. You might have guessed that
<tt>it_should_initialize_assign</tt> and <tt>it_should_render_template</tt> work in a similar
fashion and you'd be right. Again, see the documentation for these individual methods for more
information. Lots of information in those docs.
A useful helper method that doesn't appear in any of the scaffolding is <tt>with_default_restful_actions</tt>
which takes a block and evaluates it for each of the RESTful controller actions. Very useful for
spec'ing that these methods redirect to the login page when the user isn't logged in, for example. This
method is designed to be used inside an example like so:
describe "when not logged in" do
it "should redirect all requests to the login page" do
with_default_restful_actions do
response.should redirect_to(login_url)
end
end
end
Before we're through with the controller specs, let me point out one more important detail. In
order to use <tt>it_should_redirect_to</tt> we have to send the routing inside a block argument
there so it can be evaluated in the example context instead of the example group, where it
completely blows up. This methodology is used anywhere routing is referred to in a "skinny",
example group level spec.
=== View Specs
Now let's move to the view specs!
describe "/users/form.html.haml" do
before(:each) do
@user = mock_and_assign(User, :stub => {
:name => "foo",
:birthday => 1.week.ago,
:adult => false
})
end
it_should_have_form_for :user
it_should_allow_editing :user, :name
it_should_allow_editing :user, :birthday
it_should_allow_editing :user, :adult
it_should_link_to_show :user
it_should_link_to { users_path }
end
Like the special <tt>stub_index</tt> methods in the controller
specs, the view specs have a shorthand mock and stub helpers: <tt>mock_and_assign</tt> and
<tt>mock_and_assign_collection</tt>. These are well documented so please check them out.
There are also some really nice helper methods that I'd like point out. First is
<tt>it_should_have_form_for</tt>. This is a really good convenience wrapper that basically wraps
the much longer:
it "should use form_for to generate the proper form action and options" do
template.should_receive(:form_for).with(@user)
do_render
end
Next up is the <tt>it_should_allow_editing</tt> helper. I love this method the most because it
really helps DRY up that view spec while at the same time being amazingly unbrittle. Instead of
creating an expectation for a specific form element, this method creates a generalized expectation
that there's a form element with the <tt>name</tt> attribute set in such away that it will
generate the proper <tt>params</tt> to use in the controller to edit or create the instance.
Check out the docs and the source for more information on this. Also check out
<tt>it_should_have_form_element_for</tt> which is roughly equivalent for those times when you use
<tt>form_tag</tt> instead.
Finally let's look at those <tt>it_should_link_to_<i>controller_method</i></tt> helpers.
These methods (and there's one each for the controller methods
<tt>new</tt>, <tt>edit</tt>, <tt>show</tt>, and <tt>delete</tt>) point to instance variables
which you should be created in the "before" blocks with <tt>mock_and_assign</tt>. The other is
<tt>it_should_allow_editing</tt> which is likewise covered extensively in the documentation and
I will just point out here that, like <tt>it_should_link_to_edit</tt> and such, it takes a
symbol for the name of the instance variable it refers to and <i>additionally</i> takes
a symbol for the name of the attribute to be edited.
Also note that, when constructing a long form example, instead of defining an instance variable
for the name of the template and calling <tt>render @that_template</tt> you can simply call
<tt>do_render</tt> which takes the name of the template from the outermost example group where
it is customarily stated.
=== Model Specs
Skinny Spec adds a matcher for the various ActiveRecord associations. On the example group level
you call them like:
it_should_belong_to :manager
it_should_have_many :clients
Within an example you can call them on either the class or the instance setup in the
"before" block. These are equivalent:
@user.should belong_to(:group)
User.should belong_to(:group)
I've also added some very basic validation helpers like <tt>it_should_validate_presence_of</tt>,
<tt>it_should_validate_uniqueness_of</tt>, <tt>it_should_not_mass_assign</tt>. Please consult
the documentation for more information.
== Miscellaneous Notes
In the scaffolding, I have used my own idiomatic Rails usage:
* All controller actions which use HTML forms [<tt>new</tt>, <tt>edit</tt>, etc] use a shared
form and leverage <tt>form_for</tt> to its fullest by letting it create the appropriate
action and options.
* Some instances where you might expect link_to are button_to. This is to provide a common
interface element which can be styled the same instead of a mishmash of links and buttons and
inputs everywhere. To take full advantage of this, I usually override many of Rails' default
helpers with custom ones that all use actual HTML <tt>BUTTON</tt> elements which are much
easier to style than "button" typed <tt>INPUT</tt>. I've provided a text file in the
"additional" folder of this plugin which you can use in your ApplicationHelper. (I also
provide an optional override helper for the <tt>label</tt> method which uses
<tt>#titleize</tt> instead of <tt>humanize</tt> for stylistic reasons).
* Probably more that I can't think of.
== Credits and Thanks
Sections of this code were taken from or inspired by Rick Olsen's
rspec_on_rails_on_crack[http://github.com/technoweenie/rspec_on_rails_on_crack/tree/master].
Also thanks and props to Hampton Catlin and Nathan Weizenbaum for the lovely and imminently useable
Haml and make_resourceful. Also also praises and glory to David Chelimsky and the Rspec crew.
Also thanks to Don Petersen, Nicolas Mérouze, Mikkel Malmberg, and Brandan Lennox for their suggestions and fixes.

View file

@ -0,0 +1,11 @@
require 'rake'
require 'rake/rdoctask'
desc 'Generate documentation for the Skinny Spec plugin'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Skinny Spec'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,58 @@
# Please insert these into your ApplicationHelper
# Replacement for Rails' default submit_tag helper
# using HTML button element rather than HTML input element
def submit_tag(text, options = {})
content_tag :button, text, options.merge(:type => :submit)
end
# Replacement for Rails' default button_to helper
# using HTML button element rather than HTML input element
def button_to(name, options = {}, html_options = {})
html_options = html_options.stringify_keys
convert_boolean_attributes!(html_options, %w( disabled ))
method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end
form_method = method.to_s == 'get' ? 'get' : 'post'
request_token_tag = ''
if form_method == 'post' && protect_against_forgery?
request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
end
if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
url = options.is_a?(String) ? options : self.url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + content_tag("button", name, html_options) + request_token_tag + "</div></form>"
end
# Replacement for Rails' default button_to_function helper
# using HTML button element rather than HTML input element
def button_to_function(name, *args, &block)
html_options = args.extract_options!
function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given?
content_tag(:button, name, html_options.merge({
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
# Replacement for Rails' default label helper
# using String#titleize rather than String#humanize
def label(object_name, method, text = nil, options = {})
text ||= method.to_s[].titleize
super
end

View file

@ -0,0 +1,102 @@
class SkinnyScaffoldGenerator < Rails::Generator::NamedBase
attr_reader :controller_class_path, :controller_file_path, :controller_class_nesting,
:controller_class_nesting_depth, :controller_class_name, :controller_underscore_name,
:controller_plural_name, :template_language
alias_method :controller_file_name, :controller_underscore_name
alias_method :controller_singular_name, :controller_file_name
alias_method :controller_table_name, :controller_plural_name
default_options :skip_migration => false
def initialize(runtime_args, runtime_options = {})
super
base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@name.pluralize)
@controller_class_name_without_nesting, @controller_underscore_name, @controller_plural_name = inflect_names(base_name)
if @controller_class_nesting.empty?
@controller_class_name = @controller_class_name_without_nesting
else
@controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
end
end
def manifest
record do |m|
# Check for class naming collisions
m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}Helper"
m.class_collisions class_path, "#{class_name}"
# # Controller, helper, and views directories
m.directory File.join('app', 'views', controller_class_path, controller_file_name)
m.directory File.join('spec', 'views', controller_class_path, controller_file_name)
m.directory File.join('app', 'helpers', controller_class_path)
m.directory File.join('spec', 'helpers', controller_class_path)
m.directory File.join('app', 'controllers', controller_class_path)
m.directory File.join('spec', 'controllers', controller_class_path)
m.directory File.join('app', 'models', class_path)
m.directory File.join('spec', 'models', class_path)
# Views
@template_language = defined?(Haml) ? "haml" : "erb"
%w{index show form}.each do |action|
m.template "#{action}.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}")
m.template "#{action}.html_spec.rb",
File.join('spec/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}_spec.rb")
end
m.template "index_partial.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}")
m.template 'index_partial.html_spec.rb',
File.join('spec/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}_spec.rb")
# Helper
m.template 'helper.rb',
File.join('app/helpers', controller_class_path, "#{controller_file_name}_helper.rb")
m.template 'helper_spec.rb',
File.join('spec/helpers', controller_class_path, "#{controller_file_name}_helper_spec.rb")
# Controller
m.template 'controller.rb',
File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
m.template 'controller_spec.rb',
File.join('spec/controllers', controller_class_path, "#{controller_file_name}_controller_spec.rb")
# Model
m.template 'model.rb',
File.join('app/models', class_path, "#{file_name}.rb")
m.template 'model_spec.rb',
File.join('spec/models', class_path, "#{file_name}_spec.rb")
# Routing
m.route_resources controller_file_name
unless options[:skip_migration]
m.migration_template(
'migration.rb', 'db/migrate',
:assigns => {
:migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}",
:attributes => attributes
},
:migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
)
end
end
end
protected
def banner
"Usage: #{$0} skinny_scaffold ModelName [field:type, field:type]"
end
def add_options!(opt)
opt.separator ''
opt.separator 'Options:'
opt.on("--skip-migration",
"Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
end
def model_name
class_name.demodulize
end
end

View file

@ -0,0 +1,105 @@
class <%= controller_class_name %>Controller < ApplicationController
<%- if defined?(Resourceful::Maker) -%>
make_resourceful do
actions :all
# Let's get the most use from form_for and share a single form here!
response_for :new, :edit do
render :template => "<%= plural_name %>/form"
end
response_for :create_fails, :update_fails do
flash[:error] = "There was a problem!"
render :template => "<%= plural_name %>/form"
end
end
<%- else -%>
# GET /<%= table_name %>
# GET /<%= table_name %>.xml
def index
@<%= table_name %> = <%= class_name %>.find(:all)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @<%= table_name %> }
end
end
# GET /<%= table_name %>/1
# GET /<%= table_name %>/1.xml
def show
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/new
# GET /<%= table_name %>/new.xml
def new
@<%= file_name %> = <%= class_name %>.new
respond_to do |format|
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/1/edit
def edit
@<%= file_name %> = <%= class_name %>.find(params[:id])
render :template => "<%= plural_name %>/form"
end
# POST /<%= table_name %>
# POST /<%= table_name %>.xml
def create
@<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>])
respond_to do |format|
if @<%= file_name %>.save
flash[:notice] = '<%= class_name %> was successfully created.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# PUT /<%= table_name %>/1
# PUT /<%= table_name %>/1.xml
def update
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
if @<%= file_name %>.update_attributes(params[:<%= file_name %>])
flash[:notice] = '<%= class_name %> was successfully updated.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { head :ok }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /<%= table_name %>/1
# DELETE /<%= table_name %>/1.xml
def destroy
@<%= file_name %> = <%= class_name %>.find(params[:id])
@<%= file_name %>.destroy
respond_to do |format|
flash[:notice] = '<%= class_name %> was successfully deleted.'
format.html { redirect_to(<%= table_name %>_url) }
format.xml { head :ok }
end
end
<%- end -%>
end

View file

@ -0,0 +1,93 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe <%= controller_class_name %>Controller do
describe "GET :index" do
before(:each) do
@<%= plural_name %> = stub_index(<%= class_name %>)
end
it_should_find_and_assign :<%= plural_name %>
it_should_render_template "index"
end
describe "GET :new" do
before(:each) do
@<%= singular_name %> = stub_new(<%= class_name %>)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "POST :create" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>)
end
it_should_initialize_and_save :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>, :return => :false)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "GET :show" do
before(:each) do
@<%= singular_name %> = stub_show(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "show"
end
describe "GET :edit" do
before(:each) do
@<%= singular_name %> = stub_edit(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "PUT :update" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>)
end
it_should_find_and_update :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>, :return => :false)
end
it_should_find_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "DELETE :destroy" do
before(:each) do
@<%= singular_name %> = stub_destroy(<%= class_name %>)
end
it_should_find_and_destroy :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= plural_name %>_url }
end
end

View file

@ -0,0 +1,25 @@
<h1><%= singular_name %>.new_record? ? "New" : "Edit" %> <%= model_name %></h1>
<%% form_for(@<%= singular_name %>) do |f| %>
<div id="form_errors">
<%%= f.error_messages %>
</div>
<%- if attributes.blank? -%>
<p>Add your form elements here, please!</p>
<%- else -%>
<%- attributes.each do |attribute| -%>
<p>
<%%= f.label :<%= attribute.name %> %><br />
<%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
</p>
<%- end -%>
<%- end -%>
<div id="commands">
<%%= submit_tag "Save" %>
<div id="navigation_commands">
<%% unless @<%= singular_name %>.new_record? -%>
<%%= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost." %>
<%% end -%>
<%%= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes" %>
</div>
</div>
<%% end -%>

View file

@ -0,0 +1,18 @@
%h1== #{@<%= singular_name %>.new_record? ? "New" : "Edit"} #{<%= model_name %>}
- form_for @<%= singular_name %> do |f|
#form_errors= f.error_messages
<% if attributes.blank? -%>
%p Add your form elements here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
= f.label :<%= attribute.name %>
= f.<%= attribute.field_type %> :<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= submit_tag "Save"
#navigation_commands
- unless @<%= singular_name %>.new_record?
= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost."
= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes"

View file

@ -0,0 +1,40 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/form.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :address => "815 Oceanic Drive"
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
})
end
it_should_have_form_for :<%= singular_name %>
<% if attributes.blank? -%>
# Add specs for editing attributes here, please! Like this:
#
# it_should_allow_editing :<%= singular_name %>, :foo
<% else -%>
<%- attributes.each do |attribute| -%>
it_should_allow_editing :<%= singular_name %>, :<%= attribute.name %>
<%- end -%>
<% end -%>
it_should_link_to_show :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,2 @@
module <%= controller_class_name %>Helper
end

View file

@ -0,0 +1,5 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../spec_helper'
describe <%= controller_class_name %>Helper do
# Add your specs here or remove this file completely, please!
end

View file

@ -0,0 +1,31 @@
<h1><%= model_name %> List</h1>
<table>
<%%- if @<%= plural_name %>.empty? -%>
<tbody>
<tr class="empty">
<td>There are no <%= plural_name.humanize.downcase %></td>
</tr>
</tbody>
<%%- else -%>
<thead>
<tr>
<%- if attributes.blank? -%>
<th><!-- Generic display column --></th>
<%- else -%>
<%- attributes.each do |attribute| -%>
<th><%= attribute.name.titleize %></th>
<%- end -%>
<%- end -%>
<th class="show"><!-- "Show" link column --></th>
<th class="edit"><!-- "Edit" link column --></th>
<th class="delete"><!-- "Delete" link column --></th>
</tr>
</thead>
<tbody>
<%%= render :partial => @<%= plural_name %> %>
</tbody>
<%%- end -%>
</table>
<div id="commands">
<%%= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,23 @@
%h1 <%= model_name %> List
%table
- if @<%= plural_name %>.empty?
%tbody
%tr.empty
%td== There are no <%= plural_name.humanize.downcase %>
- else
%thead
%tr
<% if attributes.blank? -%>
%th= # Generic display column
<% else -%>
<%- attributes.each do |attribute| -%>
%th <%= attribute.name.titleize %>
<%- end -%>
<% end -%>
%th.show= # 'Show' link column
%th.edit= # 'Edit' link column
%th.delete= # 'Delete' link column
%tbody
= render :partial => @<%= plural_name %>
#commands
= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get"

View file

@ -0,0 +1,15 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/index.html.<%= template_language %>" do
before(:each) do
@<%= plural_name %> = mock_and_assign_collection(<%= model_name %>)
template.stub! :render
end
it "should render :partial => @<%= plural_name %>" do
template.should_receive(:render).with(:partial => @<%= plural_name %>)
do_render
end
it_should_link_to_new :<%= singular_name %>
end

View file

@ -0,0 +1,12 @@
<tr class="<%%= cycle("odd", "even") %>">
<% if attributes.blank? -%>
<td><%= model_name %> #<%%= <%= singular_name %>.id %></td>
<% else -%>
<%- attributes.each do |attribute| -%>
<td><%%=h <%= singular_name %>.<%= attribute.name %> %></td>
<%- end %>
<% end -%>
<td class="show"><%%= button_to "Show", <%= singular_name %>, :method => "get" %></td>
<td class="edit"><%%= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get" %></td>
<td class="delete"><%%= button_to "Delete", <%= singular_name %>, :method => "delete" %></td>
</tr>

View file

@ -0,0 +1,11 @@
%tr{:class => cycle("odd", "even")}
<% if attributes.blank? -%>
%td== <%= model_name %> #{<%= singular_name %>.id}
<% else -%>
<%- attributes.each do |attribute| -%>
%td=h <%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
%td.show= button_to "Show", <%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.edit= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.delete= button_to "Delete", <%= singular_name %>, :method => "delete"

View file

@ -0,0 +1,31 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%= singular_name %>.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
<% end -%>
})
template.stub!(:<%= singular_name %>).and_return(@<%= singular_name %>)
end
it_should_link_to_show :<%= singular_name %>
it_should_link_to_edit :<%= singular_name %>
it_should_link_to_delete :<%= singular_name %>
end

View file

@ -0,0 +1,14 @@
class <%= migration_name %> < ActiveRecord::Migration
def self.up
create_table :<%= table_name %>, :force => true do |t|
<% attributes.each do |attribute| -%>
t.column :<%= attribute.name %>, :<%= attribute.type %>
<% end -%>
t.timestamps
end
end
def self.down
drop_table :<%= table_name %>
end
end

View file

@ -0,0 +1,2 @@
class <%= class_name %> < ActiveRecord::Base
end

View file

@ -0,0 +1,25 @@
require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../spec_helper'
describe <%= class_name %> do
def valid_attributes(args = {})
{
# Add valid attributes for building your model instances here!
}.merge(args)
end
before(:each) do
@<%= singular_name %> = <%= class_name %>.new
end
after(:each) do
@<%= singular_name %>.destroy
end
# Add your model specs here, please!
# And don't forget about the association matchers built-in to skinny_spec like:
#
# it_should_have_many :foos
# it_should_validate_presence_of :bar
#
# Check out the docs for more information.
end

View file

@ -0,0 +1,15 @@
<h1>Show <%= model_name %></h1>
<% if attributes.blank? -%>
<p>Add your customized markup here, please!</p>
<% else -%>
<%- attributes.each do |attribute| -%>
<p>
<label><%= attribute.name.titleize %>:</label>
<%%=h @<%= singular_name %>.<%= attribute.name %> %>
</p>
<%- end -%>
<% end -%>
<div id="commands">
<%%= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get" %>
<%%= button_to "Back to List", <%= plural_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,13 @@
%h1== Show #{<%= model_name %>}
<% if attributes.blank? -%>
%p Add your customized markup here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
%label <%= attribute.name.titleize %>:
=h @<%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get"
= button_to "Back to List", <%= plural_name %>_path, :method => "get"

View file

@ -0,0 +1,31 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/show.html.<%= template_language %>" do
before(:each) do
<% if attributes.blank? -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>)
<% else -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
})
<% end -%>
end
# Add your specs here, please! But remember not to make them brittle
# by specing specing specific HTML elements and classes.
it_should_link_to_edit :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,83 @@
module LuckySneaks
# These methods are mostly just called internally by various other spec helper
# methods but you're welcome to use them as needed in your own specs.
module CommonSpecHelpers
# Stubs out Time.now and returns value to use when comparing it. Example:
#
# time_now = stub_time_now
# @foo.some_method_that_resets_updated_at
# @foo.updated_at.should == time_now
def stub_time_now
returning Time.now do |now|
Time.stub!(:now).and_return(now)
end
end
# Returns class for the specified name. Example:
#
# class_for("foo") # => Foo
def class_for(name)
name.to_s.constantize
rescue NameError
name.to_s.pluralize.classify.constantize
# Let any other error rise!
end
# Returns instance variable for the specified name. Example:
#
# instance_for("foo") # => @foo
def instance_for(name)
instance_variable_get("@#{name.to_s.underscore}")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path.
def have_link_to(path)
have_tag("a[href='#{path}']")
end
# Returns dummy value for specified attribute based on the datatype expected for that
# attribute.
def dummy_value_for(instance, attribute)
if datatype = instance.column_for_attribute(attribute)
actual = instance.send(attribute)
case datatype.type
when :string, :text
actual == "foo" ? "bar" : "food"
when :integer, :float, :decimal
actual == 108 ? 815 : 108
when :boolean
actual ? false : true
when :date, :datetime, :time, :timestamp
actual == 1.week.ago ? 2.years.ago : 1.week.ago
end
end
end
# Returns class description text
def class_description_text
if self.class.respond_to?(:description_text)
# Old school
self.class.description_text
else
# New school
self.class.description
end
end
# Returns description text
def self_description_text
if respond_to?(:description_text)
# Old school
description_text
else
# New school
description
end
end
def described_type
self.class.described_type
end
end
end

View file

@ -0,0 +1,67 @@
module LuckySneaks
module ControllerRequestHelpers # :nodoc:
def self.included(base)
base.extend ExampleGroupMethods
end
private
def define_implicit_request(method)
@controller_method = method
@implicit_request = case method
when :index, :new, :show, :edit
proc { get method, params }
when :create
proc { post :create, params }
when :update
proc { put :update, params }
when :destroy
proc { put :destroy, params }
end
end
def eval_request
instance_eval &self.class.instance_variable_get("@the_request")
rescue ArgumentError # missing block
try_shared_request_definition
end
alias do_request eval_request
def try_shared_request_definition
if defined?(shared_request) == "method"
shared_request
elsif @implicit_request
try_implicit_request
else
error_message = "Could not determine request definition for 'describe' context. "
error_message << "Please use define_request or define a shared_request."
raise ArgumentError, error_message
end
end
def try_implicit_request
@implicit_request.call
end
def get_response(&block)
eval_request
block.call(response) if block_given?
response
end
module ExampleGroupMethods
# Defines a request at the example group ("describe") level to be evaluated in the examples. Example:
#
# define_request { get :index, params }
#
# <b>Note:</b> The following methods all define implicit requests: <tt>stub_index</tt>, <tt>stub_new</tt>,
# <tt>stub_create</tt>, <tt>stub_show</tt>, <tt>stub_edit</tt>, <tt>stub_update</tt>, and
# <tt>stub_destroy</tt>. Using them in your <tt>before</tt> blocks will allow you to forego
# defining explicit requests using <tt>define_request</tt>. See
# LuckySneaks::ControllerStubHelpers for information on these methods.
def define_request(&block)
raise ArgumentError, "Must provide a block to define a request!" unless block_given?
@the_request = block
end
end
end
end

View file

@ -0,0 +1,571 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
module ControllerSpecHelpers # :nodoc:
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ControllerRequestHelpers
include LuckySneaks::ControllerStubHelpers
def self.included(base)
base.extend ExampleGroupMethods
base.extend ControllerRequestHelpers::ExampleGroupMethods
end
# Evaluates the specified block for each of the RESTful controller methods.
# This is useful to spec that all controller methods redirect when no user is
# logged in.
def with_default_restful_actions(params = {}, &block)
{
:get => :index,
:get => :new,
:post => :create
}.each do |method_id, message|
self.send method_id, message, params
block.call
end
{
:get => :edit,
:put => :update,
:delete => :destroy
}.each do |method_id, message|
if params[:before]
params.delete(:before).call
end
# Presuming any id will do
self.send method_id, message, params.merge(:id => 1)
block.call
end
end
private
def create_ar_class_expectation(name, method, argument = nil, options = {})
args = []
unless options.delete(:only_method)
args << argument unless argument.nil?
args << hash_including(options) unless options.empty?
end
method = options.delete(:find_method) if options[:find_method]
if args.empty?
class_for(name).should_receive(method).and_return(instance_for(name))
else
class_for(name).should_receive(method).with(*args).and_return(instance_for(name))
end
end
def create_positive_ar_instance_expectation(name, method, *args)
instance = instance_for(name)
if args.empty?
instance.should_receive(method).and_return(true)
else
instance.should_receive(method).with(*args).and_return(true)
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations.
module ExampleGroupMethods
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.find</tt>.
# Examples:
#
# it_should_find :foos # => Foo.should_receive(:find).with(:all)
# it_should_find :foos, :all # An explicit version of the above
# it_should_find :foos, :conditions => {:foo => "bar"} # => Foo.should_receive(:find).with(:all, :conditions => {"foo" => "bar"}
# it_should_find :foos, "joe", :method => :find_all_by_name # Foo.should_receive(:find_all_by_name).with("joe")
# it_should_find :foo # => Foo.should_recieve(:find).with(@foo.id.to_s)
# it_should_find :foo, :params => "id" # => Foo.should_receive(:find).with(params[:id].to_s)
# it_should_find :foo, 2 # => Foo.should_receive(:find).with("2")
# it_should_find :foo, "joe", :method => :find_by_name # => Foo.should_recieve(:find_by_name).with("joe")
#
# <b>Note:</b> All params (key and value) will be strings if they come from a form element and are handled
# internally with this expectation.
def it_should_find(name, *args)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should find #{name_message}" do
options = args.extract_options!
# Blech!
argument = if param = params[options.delete(:params)]
param.to_s
else
if args.first
args.first
elsif (instance = instance_variable_get("@#{name}")).is_a?(ActiveRecord::Base)
instance.id.to_s
else
:all
end
end
find_method = options.delete(:method) || :find
create_ar_class_expectation name, find_method, argument, options
eval_request
end
end
# Negative version of <tt>it_should_find</tt>. This creates an expectation that
# the class never receives <tt>find</tt> at all.
def it_should_not_find(name)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should not find #{name_message}" do
if name_string == name_string.singularize
class_for(name).should_not_receive(:find)
else
class_for(name).should_not_receive(:find).with(:all)
end
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.new</tt>.
# Takes optional <tt>params</tt> for the initialization arguments. Example
#
# it_should_initialize :foo # => Foo.should_receive(:new)
# it_should_initialize :foo, :params => :bar # => Foo.should_receive(:new).with(params[:bar])
# it_should_initialize :foo, :bar => "baz" # => Foo.should_receive(:new).with(:bar => "baz")
def it_should_initialize(name, options = {})
it "should initialize a #{name}" do
create_ar_class_expectation name, :new, params[options.delete(:params)], options
eval_request
end
end
# Negative version of <tt>it_should_initialize</tt>. This creates an expectation
# that the class never recieves <tt>new</tt> at all.
def it_should_not_initialize(name)
it "should initialize a #{name}" do
class_for(name).should_not_receive(:new)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#save</tt> on the
# named instance. Example:
#
# it_should_save :foo # => @foo.should_receive(:save).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>save</tt> call. Use <tt>it_should_assign</tt>
# instead, to verify that the instance is captured in an instance variable for the inevitable re-rendering
# of the form template.
def it_should_save(name)
it "should save the #{name}" do
create_positive_ar_instance_expectation name, :save
eval_request
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>save</tt> at all.
def it_should_not_save(name)
it "should not save the #{name}" do
instance_for(name).should_not_receive(:save)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#update_attributes</tt>
# on the named instance. Takes optional argument for <tt>params</tt> to specify in the
# expectation. Examples:
#
# it_should_update :foo # => @foo.should_receive(:update_attributes).and_return(true)
# it_should_update :foo, :params => :bar # => @foo.should_receive(:update_attributes).with(params[:bar]).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>update_attributes</tt> call. Use
# <tt>it_should_assign</tt> instead, to verify that the instance is captured in an instance variable
# for the inevitable re-rendering of the form template.
def it_should_update(name, options = {})
it "should update the #{name}" do
create_positive_ar_instance_expectation name, :update_attributes, params[name]
eval_request
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>update_attributes</tt> at all.
def it_should_not_update(name)
it "should not update the #{name}" do
instance_for(name).should_not_receive(:update_attributes)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#destroy</tt> on the named
# instance. Example:
#
# it_should_destroy :foo # => @foo.should_receive(:destroy).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>destroy</tt> call. Use
# <tt>it_should_assign</tt> instead, if you need to verify that the instance is captured in an instance
# variable if it is re-rendered somehow. This is probably a really edge use case.
def it_should_destroy(name, options = {})
it "should delete the #{name}" do
create_positive_ar_instance_expectation name, :destroy
eval_request
end
end
# Negative version of <tt>it_should_destroy</tt>. This creates an expectation
# that the instance never receives <tt>destroy</tt> at all.
def it_should_not_destroy(name)
it "should not destroy the #{name}" do
instance_for(name).should_not_receive(:destroy)
eval_request
end
end
# Creates expectation[s] that the controller method should assign the specified
# instance variables along with any specified values. Examples:
#
# it_should_assign :foo # => assigns[:foo].should == @foo
# it_should_assign :foo => "bar" # => assigns[:foo].should == "bar"
# it_should_assign :foo => :nil # => assigns[:foo].should be_nil
# it_should_assign :foo => :not_nil # => assigns[:foo].should_not be_nil
# it_should_assign :foo => :undefined # => controller.send(:instance_variables).should_not include("@foo")
#
# Very special thanks to Rick Olsen for the basis of this code. The only reason I even
# redefine it at all is purely an aesthetic choice for specs like "it should foo"
# over ones like "it foos".
def it_should_assign(*names)
names.each do |name|
if name.is_a?(Symbol)
it_should_assign name => name
elsif name.is_a?(Hash)
name.each do |key, value|
it_should_assign_instance_variable key, value
end
end
end
end
# Essentially shorthand for <tt>it_should_assign name => :nil</tt>. This method can take multiple
# instance variable names, creating this shorthand for each name. See the docs for
# <tt>it_should_assign</tt> for more information.
def it_should_not_assign(*names)
names.each do |name|
# Assuming name is a symbol
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_assign(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_find_and_assign</tt>. This creates an
# expectation that the class never receives <tt>find</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_find_and_assign(*names)
names.each do |name|
it_should_not_find name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>new</tt>, where the instance
# is initialized without being saved (this includes failed <tt>create</tt> requests).
# If you want to spec that the controller method successfully saves the instance,
# please use <tt>it_should_initialize_and_save</tt>.
def it_should_initialize_and_assign(*names)
names.each do |name|
it_should_initialize name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_initialize_and_assign</tt>. This creates an
# expectation that the class never receives <tt>new</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_initialize_and_assign(*names)
names.each do |name|
it_should_not_initialize name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_save</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>create</tt>, where the instance
# is initialized and successfully saved. If you want to spec that the instance is created
# but not saved, just use <tt>it_should_initialize_and_assign</tt>.
def it_should_initialize_and_save(*names)
names.each do |name|
it_should_initialize name, :only_method => true
it_should_save name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_update</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>update</tt>, where the
# instance is loaded from the database and successfully saved. If you want to spec that the
# instance is found but not saved, just use <tt>it_should_find_and_assign</tt>.
def it_should_find_and_update(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_update name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_destroy</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_destroy(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_destroy name
end
end
# Creates an expectation that the specified collection (<tt>flash</tt>, <tt>session</tt>,
# <tt>params</tt>, <tt>cookies</tt>) contains the specified key and value. To specify that
# the collection should be set to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set(collection, key, value = nil, &block)
it "should set #{collection}[:#{key}]#{' with ' + value.inspect if value}" do
# Allow flash.now[:foo] to remain in the flash
flash.stub!(:sweep) if collection == :flash
eval_request
if value
if value == :nil
self.send(collection)[key].should be_nil
else
self.send(collection)[key].should == value
end
elsif block_given?
self.send(collection)[key].should == instance_eval(&block)
else
self.send(collection)[key].should_not be_nil
end
end
end
# Wraps <tt>it_should_set :flash</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_flash(name, value = nil, &block)
it_should_set :flash, name, value, &block
end
# Wraps <tt>it_should_set :flash, :nil</tt>.
def it_should_not_set_flash(name)
it_should_set :flash, name, :nil
end
# Wraps <tt>it_should_set :session</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_session(name, value = nil, &block)
it_should_set :session, name, value, &block
end
# Wraps <tt>it_should_set :session, :nil</tt>.
def it_should_not_set_session(name)
it_should_set :session, name, :nil
end
# Wraps <tt>it_should_set :params</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_params(name, value = nil, &block)
it_should_set :params, name, value, &block
end
# Wraps <tt>it_should_set :params, :nil</tt>.
def it_should_not_set_params(name)
it_should_set :params, name, :nil
end
# Wraps <tt>it_should_set :cookies</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_cookies(name, value = nil, &block)
it_should_set :cookies, name, value, &block
end
# Wraps <tt>it_should_set :cookies, :nil</tt>.
def it_should_not_set_cookies(name)
it_should_set :cookies, name, :nil
end
# Wraps the various <tt>it_should_render_<i>foo</i></tt> methods:
# <tt>it_should_render_template</tt>, <tt>it_should_render_partial</tt>,
# <tt>it_should_render_xml</tt>, <tt>it_should_render_json</tt>,
# <tt>it_should_render_formatted</tt>, and <tt>it_should_render_nothing</tt>.
def it_should_render(render_method, *args)
send "it_should_render_#{render_method}", *args
end
# Creates an expectation that the controller method renders the specified template.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_template(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' template" do
eval_request
response.should render_template(name)
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified partial.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_partial(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' partial" do
controller.expect_render(:partial => name)
eval_request
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified record via <tt>to_xml</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_xml(record = nil, options = {}, &block)
it_should_render_formatted :xml, record, options, &block
end
# Creates an expectation that the controller method renders the specified record via <tt>to_json</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_json(record = nil, options = {}, &block)
it_should_render_formatted :json, record, options, &block
end
# Called internally by <tt>it_should_render_xml</tt> and <tt>it_should_render_json</tt>
# but should not really be called much externally unless you have defined your own
# formats with a matching <tt>to_foo</tt> method on the record.
#
# Which is probably never.
def it_should_render_formatted(format, record = nil, options = {}, &block)
create_status_expectation options[:status] if options[:status]
it "should render #{format.inspect}" do
if record.is_a?(Hash)
options = record
record = nil
end
if record.nil? && !block_given?
raise ArgumentError, "it_should_render must be called with either a record or a block and neither was given."
else
if record
pieces = record.to_s.split(".")
record = instance_variable_get("@#{pieces.shift}")
record = record.send(pieces.shift) until pieces.empty?
end
block ||= proc { record.send("to_#{format}") }
get_response do |response|
response.should have_text(block.call)
end
end
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method returns a blank page. You'd already
# know when and why to use this so I'm not typing it out.
def it_should_render_nothing(options = {})
create_status_expectation options[:status] if options[:status]
it "should render :nothing" do
get_response do |response|
response.body.strip.should be_blank
end
end
end
# Creates an expectation that the controller method redirects to the specified destination. Example:
#
# it_should_redirect_to { foos_url }
#
# <b>Note:</b> This method takes a block to evaluate the route in the example
# context rather than the example group context.
def it_should_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should redirect to #{(hint || route)}" do
eval_request
response.should redirect_to(instance_eval(&route))
end
end
# Negative version of <tt>it_should_redirect_to</tt>.
def it_should_not_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not redirect to #{(hint || route)}" do
eval_request
response.should_not redirect_to(instance_eval(&route))
end
end
# Creates an expectation that the controller method redirects back to the previous page
def it_should_redirect_to_referer
it "should redirect to the referring page" do
request.env["HTTP_REFERER"] = "http://test.host/referer"
eval_request
response.should redirect_to("http://test.host/referer")
end
end
alias it_should_redirect_to_referrer it_should_redirect_to_referer
private
def it_should_assign_instance_variable(name, value)
expectation_proc = case value
when :nil
proc { assigns[name].should be_nil }
when :not_nil
proc { assigns[name].should_not be_nil }
when :undefined
proc { controller.send(:instance_variables).should_not include("@{name}") }
when Symbol
if (instance_variable = instance_variable_get("@#{name}")).nil?
proc { assigns[name].should_not be_nil }
else
proc { assigns[name].should == instance_variable }
end
else
proc { assigns[name].should == value }
end
it "should #{value == :nil ? 'not ' : ''}assign @#{name}" do
eval_request
instance_eval &expectation_proc
end
end
end
end
end

View file

@ -0,0 +1,238 @@
module LuckySneaks # :nodoc:
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort. The methods which correspond
# to the controller methods perform the most duties as they create the mock_model instances,
# stub out all the necessary methods, and also create implicit requests to DRY up your spec
# file even more. You are encouraged to use these methods to setup the basic calls for your
# resources and only resort to the other methods when mocking and stubbing secondary objects
# and calls.
module ControllerStubHelpers
# Stubs out <tt>find :all</tt> and returns a collection of <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>Note:</b> When specifying the method, the call is stubbed
# to accept any arguments. Caveat programmer.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:size</b>:: Number of instances to return in the result. Default is 3.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# Any additional options will be passed as arguments to the class find.
# You will want to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
def stub_find_all(klass, options = {})
returning(Array.new(options[:size] || 3){mock_model(klass)}) do |collection|
stub_out klass, options.delete(:stub)
if format = options.delete(:format)
stub_formatted collection, format
params[:format] = format
end
if find_method = options[:find_method]
# Not stubbing specific arguments here
# If you need more specificity, write a custom example
klass.stub!(find_method).and_return(collection)
else
klass.stub!(:find).with(:all).and_return(collection)
klass.stub!(:find).with(:all, hash_including(options)).and_return(collection)
end
end
end
# Alias for <tt>stub_find_all</tt> but additionally defines an implicit request <tt>get :index</tt>.
def stub_index(klass, options = {})
define_implicit_request :index
stub_find_all klass, options
end
# Stubs out <tt>new</tt> method and returns a <tt>mock_model</tt> instance marked as a new record.
# Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# It also accepts some options used to stub out <tt>save</tt> with a specified <tt>true</tt>
# or <tt>false</tt> but you should be using <tt>stub_create</tt> in that case.
def stub_initialize(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options[:format]
stub_formatted member, format
params[:format] = format
end
klass.stub!(:new).and_return(member)
if options[:params]
klass.stub!(:new).with(hash_including(options[:params])).and_return(member)
end
if options[:stub_save]
stub_ar_method member, :save, options[:return]
else
member.stub!(:new_record?).and_return(true)
member.stub!(:id).and_return(nil)
end
end
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>get :new</tt>.
def stub_new(klass, options = {})
define_implicit_request :new
stub_initialize klass, options
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>post :create</tt>.
#
# <b>Note:</b> If <tt>stub_create<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_create(klass, options = {})
define_implicit_request :create
class_name = klass.name.underscore
options[:params] ||= params[class_name]
stub_initialize klass, options.merge(:stub_save => true)
end
# Stubs out <tt>find</tt> and returns a single <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
# <b>:current_object</b>:: If set to true, <tt>find</tt> will set <tt>params[:id]</tt>
# using the <tt>id</tt> of the <tt>mock_model</tt> instance
# and use that value as an argument when stubbing <tt>find</tt>
#
# Any additional options will be passed as arguments to <tt>find</tt>.You will want
# to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
#
# <b>Note:</b> The option <tt>:stub_ar</tt> is used internally by <tt>stub_update</tt>
# and <tt>stub_destroy</tt>. If you need to stub <tt>update_attributes</tt> or
# <tt>destroy</tt> you should be using the aforementioned methods instead.
def stub_find_one(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options.delete(:format)
stub_formatted member, format
params[:format] = format
end
if options.delete(:current_object)
params[:id] = member.id
if ar_stub = options.delete(:stub_ar)
stub_ar_method member, ar_stub, options.delete(:return), options.delete(:update_params)
end
end
if find_method = options.delete(:find_method)
klass.stub!(find_method).and_return(member)
else
# Stubbing string and non-string just to be safe
klass.stub!(:find).with(member.id).and_return(member)
klass.stub!(:find).with(member.id.to_s).and_return(member)
unless options.empty?
klass.stub!(:find).with(member.id, hash_including(options)).and_return(member)
klass.stub!(:find).with(member.id.to_s, hash_including(options)).and_return(member)
end
end
end
end
# <b>Note:</b> Use of this method with :child options (to mock
# association) is deprecated. Please use <tt>stub_association</tt>.
#
# Same as <tt>stub_find_one</tt> but setups the instance as the parent
# of the specified association. Example:
#
# stub_parent(Document, :child => :comments)
#
# This stubs <tt>Document.find</tt>, <tt>@document.comments</tt> (which
# will return <tt>Comment</tt> class), as well as <tt>params[:document_id]</tt>.
# This method is meant to be used in the controller for the specified child
# (<tt>CommentsController</tt> in this instance) in situations like:
#
# def index
# @document = Document.find(params[:document_id])
# @comments = @document.comments.find(:all)
# end
def stub_parent(klass, options = {})
returning stub_find_one(klass, options) do |member|
params[klass.name.foreign_key] = member.id
if offspring = options.delete(:child)
puts "stub_parent with :child option has been marked for deprecation"
puts "please use stub_association to create the mock instead"
member.stub!(offspring).and_return(class_for(offspring))
end
end
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :show</tt>.
def stub_show(klass, options = {})
define_implicit_request :show
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :edit</tt>.
def stub_edit(klass, options = {})
define_implicit_request :edit
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>put :update</tt>
# and stubs out the <tt>update_attribute</tt> method on the instance as well.
#
# <b>Note:</b> If <tt>stub_update<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_update(klass, options = {})
define_implicit_request :update
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :update_attributes)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>delete :destroy</tt>
# and stubs out the <tt>destroy</tt> method on the instance as well.
def stub_destroy(klass, options = {})
define_implicit_request :destroy
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :destroy)
end
# Stubs <tt>to_xml</tt> or <tt>to_json</tt> respectively based on <tt>format</tt> argument.
def stub_formatted(object, format)
return unless format
object.stub!("to_#{format}").and_return("#{object.class} formatted as #{format}")
end
# Creates a mock object representing an association proxy, stubs the appropriate
# method on the parent object and returns that association proxy.
# Accepts the following option:
#
# <b>:stub</b>:: Additional methods to stub on the mock proxy object
def stub_association(object, association, options = {})
# I know options isn't implemented anywhere
object_name = instance_variables.select{|name| instance_variable_get(name) == object}
returning mock("Association proxy for #{object_name}.#{association}") do |proxy|
stub_out proxy, options[:stub] if options[:stub]
object.stub!(association).and_return(proxy)
end
end
private
# Stubs out multiple methods. You shouldn't be calling this yourself and if you do
# you should be able to understand the code yourself, right?
def stub_out(object, stubs = {})
return if stubs.nil?
stubs.each do |method, value|
if value
object.stub!(method).and_return(value)
else
object.stub!(method)
end
end
end
# Stubs out ActiveRecord::Base methods like #save, #update_attributes, etc
# that may be called on a found or instantiated mock_model instance.
def stub_ar_method(object, method, return_value, params = {})
if params.blank?
object.stub!(method).and_return(return_value ? false : true)
else
object.stub!(method).with(hash_including(params)).and_return(return_value ? false : true)
end
end
end
end

View file

@ -0,0 +1,496 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your model specs a little more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ModelSpecHelpers::ExampleGroupLevelMethods.
#
# Also check out the methods in LuckySneaks::ModelSpecHelpers::AssociationMatcher
# for some helpful matcher helper methods to use with these methods if you want to spec
# options on your association setups.
module ModelSpecHelpers
include LuckySneaks::CommonSpecHelpers
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# These methods cannot be used alone but are used in compliment with the association
# matchers in LuckySneaks::ModelSpecHelpers like <tt>have_many</tt>. Example:
#
# describe User do
# it "should have many memberships" do
# User.should have_many(:memberships)
# end
#
# it "should have many sites through memberships" do
# User.should have_many(:sites).through(:memberships)
# end
#
# it "should belong to a manager" do
# User.should belong_to(:manager).with_counter_cache
# end
# end
#
# <b>Note:</b> To spec these sorts of options using the example block helpers like
# <tt>it_should_have_many</tt>, just add them as options directly. This will use
# <tt>with_options</tt> rather than any specific matcher helpers but will have the same
# effects. Example:
#
# describe User do
# it_should_have_many :sites, :through => :memberships
# end
class AssociationMatcher
def initialize(associated, macro) # :nodoc:
@associated = associated
@macro = macro
@options = {}
end
def matches?(main_model) # :nodoc:
unless main_model.respond_to?(:reflect_on_association)
if main_model.class.respond_to?(:reflect_on_association)
main_model = main_model.class
else
@not_model = main_model
return false
end
end
if @association = main_model.reflect_on_association(@associated)
@options.all?{|k, v| @association.options[k] == v ||
[@association.options[k]] == v} # Stupid to_a being obsoleted!
end
end
def failure_message # :nodoc:
if @not_model
" expected: #{@not_model} to be a subclass of ActiveRecord::Base class, but was not"
elsif @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)}, but the association does not exist"
end
end
def negative_failure_message # :nodoc:
if @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)} to not occur but it does"
end
end
# The following public methods are chainable extensions on the main matcher
# Examples:
#
# Foo.should have_many(:bars).through(:foobars).with_dependent(:destroy)
# Bar.should belong_to(:baz).with_class_name("Unbaz")
def through(through_model)
@options[:through] = through_model
self
end
def and_includes(included_models)
@options[:include] = included_models
self
end
def and_extends(*modules)
@options[:extends] = modules
self
end
def with_counter_cache(counter_cache = true)
if counter_cache
@options[:counter_cache] = counter_cache
end
self
end
def uniq(*irrelevant_args)
@options[:uniq] = true
self
end
alias and_is_unique uniq
alias with_unique uniq
def polymorphic(*irrelevant_args)
@options[:polymorphic] = true
self
end
alias and_is_polymorphic polymorphic
alias with_polymorphic polymorphic
def as(interface)
@options[:as] = interface
end
# Use this to just specify the options as a hash.
# Note: It will completely override any previously set options
def with_options(options = {})
options.each{|k, v| @options[k] = v}
self
end
private
# Takes care of methods like with_dependent(:destroy)
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if method_name =~ /^with_(.*)/
@options[$1.to_sym] = args
self
else
super method_id, *args, &block
end
end
def association_with(options)
option_string = (options.nil? || options.empty?) ? "" : options.inspect
unless option_string.blank?
option_string.sub! /^\{(.*)\}$/, ', \1'
option_string.gsub! /\=\>/, ' => '
end
"#{@macro} :#{@associated}#{option_string}"
end
end
# Creates matcher that checks if the receiver has a <tt>belongs_to</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def belong_to(model)
AssociationMatcher.new model, :belongs_to
end
# Creates matcher that checks if the receiver has a <tt>have_one</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_one(model)
AssociationMatcher.new model, :has_one
end
# Creates matcher that checks if the receiver has a <tt>have_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_many(models)
AssociationMatcher.new models, :has_many
end
# Creates matcher that checks if the receiver has a <tt>have_and_belong_to_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_and_belong_to_many(models)
AssociationMatcher.new models, :has_and_belongs_to_many
end
private
def class_or_instance
@model_spec_class_or_instance ||= class_for(described_type) || instance
end
def instance
@model_spec_instance ||= instance_for(described_type)
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Some of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ModelSpecHelpers for more information.
#
# <b>Note:</b> The validation matchers are only meant to be used for simple validation checking
# not as a one-size-fits-all solution.
module ExampleGroupLevelMethods
# Creates an expectation that the current model being spec'd has a <tt>belong_to</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>belong_to</tt> spec like this:
#
# it_should_belong_to :document, :counter_cache => true
#
# which is the same as writing out:
#
# it "should belong to document" do
# Comment.should belong_to(:document).with_options(:counter_cache => true)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>belong_to</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_belong_to(model, options = {})
it "should belong to a #{model}" do
if options.empty?
class_or_instance.should belong_to(model)
else
class_or_instance.should belong_to(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_one</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_one</tt> spec like this:
#
# it_should_have_one :last_comment, :class_name => "Comment", :order => "created_at DESC"
#
# which is the same as writing out:
#
# it "should have one document" do
# Document.should have_one(:last_comment).with_options(:class_name => "Comment", :order => "created_at DESC")
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_one</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_one(model, options = {})
it "should have one #{model}" do
if options.empty?
class_or_instance.should have_one(model)
else
class_or_instance.should have_one(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_many</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_many</tt> spec like this:
#
# it_should_have_many :memberships, :through => :sites
#
# which is the same as writing out:
#
# it "should have many memberships" do
# User.should have_many(:memberships).with_options(:through => :sites)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_many(models, options = {})
it "should have many #{models}" do
if options.empty?
class_or_instance.should have_many(models)
else
class_or_instance.should have_many(models).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_and_belong_to_many</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_and_belong_to_many</tt> spec like this:
#
# it_should_have_and_belong_to_many :documents, :include => :attachments
#
# which is the same as writing out:
#
# it "should belong to document" do
# User.should have_and_belong_to_many(:documents).with_options(:include => :attachments)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_and_belong_to_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_and_belong_to_many(models, options = {})
it "should have and belong to many #{models}" do
if options.empty?
class_or_instance.should have_and_belong_to_many(models)
else
class_or_instance.should have_and_belong_to_many(models).with_options(options)
end
end
end
# Creates an expectation that new instances of the model being spec'd
# should initialise the specified attributes with a default value.
#
# it_should_default_attributes :status => 'new'
#
def it_should_default_attributes(hash_attribute_values)
hash_attribute_values.each_pair do |a,v|
it "should default #{a} attribute to #{v}" do
class_or_instance.new.send(a).should == v
end
end
end
# Creates an expectation that the current model being spec'd <tt>validates_presence_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_presence_of(attribute, message = default_error_message(:blank))
it "should not be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_presence_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_presence_of(attribute, message = default_error_message(:blank))
it "should be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_inclusion_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_inclusion_of(attribute, options = {}, message = default_error_message(:inclusion))
it "should validate #{attribute} is in #{options[:in].to_s}" do
# We specifically do not try to go below the range on String and character ranges because that problem set is unpredictable.
lower = options[:in].first.respond_to?(:-) ? options[:in].first - 0.0001 : nil
higher = options[:in].last.succ
instance.send "#{attribute}=", lower
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", higher
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", (lower+higher)/2
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_numericality_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_numericality_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should not validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_confirmation_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_confirmation_of(attribute, message = default_error_message(:confirmation))
it "should validate confirmation of #{attribute}" do
dummy_value = dummy_value_for(instance, attribute) || "try a string"
instance.send "#{attribute}=", dummy_value
instance.send "#{attribute}_confirmation=", dummy_value.succ
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_uniqueness_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
#
# <b>Note:</b> This method will fail completely if <tt>valid_attributes</tt>
# does not provide all the attributes needed to create a valid record.
def it_should_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should include(message)
previous_instance.destroy
end
end
# Negative version of <tt>it_should_validate_uniqueness_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should not validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should_not include(message)
previous_instance.destroy
end
end
# Creates an expectation that the current model being spec'd accepts the specified values as
# valid for the specified attribute. This is most likely used with <tt>validates_format_of</tt>
# but there's nothing saying it couldn't be another validation.
def it_should_accept_as_valid(attribute, *values)
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
instance.errors_on(attribute).should == []
end
end
end
# Creates an expectation that the current model being spec'd does not accept the specified
# values as valid for the specified attribute. This is most likely used with
# <tt>validates_format_of</tt> but there's nothing saying it couldn't be another validation.
# Takes an optional argument <tt>:message => "some custom error messsage"</tt> for
# spec'ing the actual error message.
def it_should_not_accept_as_valid(attribute, *values)
options = values.extract_options!
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should not accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
if options[:message]
instance.errors_on(attribute).should include(options[:message])
else
instance.should have_at_least(1).errors_on(attribute)
end
end
end
end
# Creates an expectation that the current model being spec'd doesn't allow mass-assignment
# of the specified attribute.
def it_should_not_mass_assign(attribute)
it "should not allow mass-assignment of #{attribute}" do
lambda {
instance.send :attributes=, {attribute => dummy_value_for(instance, attribute)}
}.should_not change(instance, attribute)
end
end
def default_error_message(attribute)
if defined?(I18n)
I18n.translate attribute, :scope => "activerecord.errors.messages"
else
ActiveRecord::Errors.default_error_messages[attribute]
end
end
end
end
end

View file

@ -0,0 +1,577 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your view specs less brittle and more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ViewSpecHelpers::ExampleGroupLevelMethods.
module ViewSpecHelpers
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ViewStubHelpers
include ActionController::PolymorphicRoutes
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element with
# its <tt>action</tt> attribute set to the specified path.
def submit_to(path)
have_tag("form[action=#{path}]")
end
# Wraps a matcher that checks if the receiver contains any of several form elements
# that would return sufficient named parameters to allow editing of the specified
# attribute on the specified instance. Example:
#
# response.should allow_editing(@foo, "bar")
#
# can be satisfied by any of the following HTML elements:
#
# <input name="foo[bar]" type="text" />
# <input name="foo[bar]" type="checkbox" />
# <input name="foo[bar_ids][]" type="checkbox" />
# <select name="foo[bar]"></select>
# <textarea name="foo[bar]"></textarea>
def allow_editing(instance, attribute)
instance_name = instance.class.name.underscore.downcase
column = instance.column_for_attribute(attribute)
if column && [Date, Time].include?(column.klass)
have_tag(
"input[name='#{instance_name}[#{attribute}]'],
select[name=?]", /#{instance_name}\[#{attribute}\(.*\)\]/
)
else
have_tag(
"input[type='text'][name='#{instance_name}[#{attribute}]'],
input[type='password'][name='#{instance_name}[#{attribute}]'],
select[name='#{instance_name}[#{attribute}]'],
textarea[name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute.to_s.tableize.singularize}_ids][]'],
input[type='radio'][name='#{instance_name}[#{attribute}]']"
)
end
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element
# whose <tt>enctype</tt> attribute is set to <tt>"multipart/form-data"<tt>
# and contains an <tt>INPUT</tt> element whose <tt>name</tt> attribute correlates
# with the provided instance and attribute.
def allow_uploading(instance, attribute)
instance_name = instance.class.name.underscore.downcase
have_tag("form[enctype='multipart/form-data'] input[type='file'][name='#{instance_name}[#{attribute}]']")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path or a <tt>FORM</tt>
# element whose <tt>action</tt> attribute is set to the specified path.
def have_link_or_button_to(path)
have_tag(
"a[href='#{path}'],
form[action='#{path}'] input,
form[action='#{path}'] button"
)
end
alias have_link_to have_link_or_button_to
alias have_button_to have_link_or_button_to
# Wraps <tt>have_link_or_button_to new_polymorphic_path<tt> for the specified class which
# corresponds with the <tt>new</tt> method of the controller.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def have_link_or_button_to_new(name)
have_link_or_button_to new_polymorphic_path(name.is_a?(ActiveRecord::Base) ? name : class_for(name))
end
# Wraps <tt>have_link_or_button_to polymorphic_path(instance)<tt> which
# corresponds with the <tt>show</tt> method of the controller.
def have_link_or_button_to_show(instance)
path = polymorphic_path(instance)
have_tag(
"a[href='#{path}'],
form[action='#{path}'][method='get'] input,
form[action='#{path}'][method='get'] button,
form[action='#{path}'] input[name='_method'][value='get'] + input,
form[action='#{path}'] input[name='_method'][value='get'] + button"
)
end
alias have_link_to_show have_link_or_button_to_show
alias have_button_to_show have_link_or_button_to_show
# Wraps <tt>have_link_or_button_to edit_polymorphic_path(instance)<tt> which
# corresponds with the <tt>edit</tt> method of the controller.
def have_link_or_button_to_edit(instance)
have_link_or_button_to edit_polymorphic_path(instance)
end
alias have_link_to_edit have_link_or_button_to_edit
alias have_button_to_edit have_link_or_button_to_edit
# Wraps a matcher that checks if the receiver contains the HTML created by Rails'
# <tt>button_to</tt> helper: to wit, a <tt>FORM</tt> element whose <tt>action</tt>
# attribute is pointed at the <tt>polymorphic_path</tt> of the instance
# and contains an <tt>INPUT</tt> named "_method" with a value of "delete".
def have_button_to_delete(instance)
path = polymorphic_path(instance)
have_tag(
"form[action='#{path}'] input[name='_method'][value='delete'] + input,
form[action='#{path}'] input[name='_method'][value='delete'] + button,
a[href=\"#{path}\"][onclick*=\"f.method = 'POST'\"][onclick*=\"m.setAttribute('name', '_method'); m.setAttribute('value', 'delete')\"]"
)
end
# Creates a <tt>mock_model</tt> instance and adds it to the <tt>assigns</tt> collection
# using either the name passed as the first argument or the underscore version
# of its class name. Accepts optional arguments to stub out additional methods
# (and their return values) on the <tt>mock_model</tt> instance. Example:
#
# mock_and_assign(Foo, :stub => {:bar => "bar"})
#
# is the same as running <tt>assigns[:foo] = mock_model(Foo, :bar => "bar")</tt>.
#
# mock_and_assign(Foo, "special_foo", :stub => {:bar => "baz"})
#
# is the same as running <tt>assigns[:special_foo] = mock_model(Foo, :bar => "baz").
#
# <b>Note:</b> Adding to the assigns collection returns the object added, so this can
# be chained a la <tt>@foo = mock_and_assign(Foo)</tt>.
def mock_and_assign(klass, *args)
options = args.extract_options!
mocked = if options[:stub]
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
self.assigns[args.first || "#{klass}".underscore] = mocked
end
# Creates an array of <tt>mock_model</tt> instances in the manner of
# <tt>mock_and_assign</tt>. Accepts <tt>option[:size]</tt> which sets the size
# of the array (default is 3).
def mock_and_assign_collection(klass, *args)
options = args.extract_options!
return_me = Array.new(options[:size] || 3) do
mocked = if options[:stub]
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
mocked
end
self.assigns[args.first || "#{klass}".tableize] = return_me
end
private
def do_render
if @the_template
render @the_template
elsif File.exists?(File.join(RAILS_ROOT, "app/views", class_description_text))
render class_description_text
else
error_message = "Cannot determine template for render. "
error_message << "Please define @the_template in the before block "
error_message << "or name your describe block so that it indicates the correct template."
raise NameError, error_message
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Most of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ViewSpecHelpers for more information.
module ExampleGroupLevelMethods
include LuckySneaks::CommonSpecHelpers
# Creates an expectation which calls <tt>submit_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a Proc to evaluate the route not simply a named route
# helper, which would be undefined in the scope of the example block.
def it_should_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should submit to #{(hint || route)}" do
do_render
response.should submit_to(instance_eval(&route))
end
end
# Negative version of <tt>it_should_submit_to</tt>. See that method for more
# details.
def it_should_not_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not submit to #{(hint || route)}" do
do_render
response.should_not submit_to(instance_eval(&route))
end
end
# Creates an expectation that the template uses Rails' <tt>form_for</tt> to generate
# the proper form action and method to create or update the specified object.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to create the expectation for <tt>form_for</tt>
# not an instance variable, which would be nil in the scope of the example block.
# If you use namespacing for your <tt>form_for</tt>, you'll have to manually write out
# a similar spec.
def it_should_have_form_for(name, options = {})
it "should have a form_for(@#{name})" do
if options.empty?
template.should_receive(:form_for).with(instance_for(name))
else
template.should_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Negative version of <tt>it_should_have_form_for</tt>. See that method for more
# details.
def it_should_not_have_form_for(name, options = {})
it "should not have a form_for(@#{name})" do
if options.empty?
template.should_not_receive(:form_for).with(instance_for(name))
else
template.should_not_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Creates an expectation which calls <tt>allow_editing</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_editing</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_editing</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_editing(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_editing</tt>. See that method for more
# details.
def it_should_not_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_editing(instance_for(instance_name), attribute)
end
end
end
# Creates an expectation which calls <tt>allow_uploading</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_uploading</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_uploading</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_uploading(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_uploading</tt>. See that method for more
# details.
def it_should_not_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_uploading(instance_for(instance_name), attribute)
end
end
end
# Creates an expectation that the rendered template contains a <tt>FORM</tt> element
# (<tt>INPUT</tt>, <tt>TEXTAREA</tt>, or <tt>SELECT</tt>) with the specified name.
def it_should_have_form_element_for(name)
it "should have a form element named '#{name}'" do
do_render
response.should have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Negative version of <tt>it_should_have_form_element_for</tt>. See that method
# for more details.
def it_should_not_have_form_element_for(name)
it "should not have a form element named '#{name}'" do
do_render
response.should_not have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Creates an expectation which calls <tt>have_link_or_button_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a block to evaluate the route in the example context
# instead of the example group context.
def it_should_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_have_link_to it_should_link_to
alias it_should_have_button_to it_should_link_to
alias it_should_have_button_or_link_to it_should_link_to
# Negative version of <tt>it_should_link_to</tt>. See that method
# for more details.
def it_should_not_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should_not have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_not_have_link_to it_should_not_link_to
alias it_should_not_have_button_to it_should_not_link_to
alias it_should_not_have_button_or_link_to it_should_not_link_to
# Creates an expectation which calls <tt>have_link_or_button_to_new</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def it_should_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should have_link_or_button_to_new(name)
end
end
alias it_should_have_link_to_new it_should_link_to_new
alias it_should_have_button_to_new it_should_link_to_new
alias it_should_have_button_or_link_to_new it_should_link_to_new
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should_not have_link_or_button_to_new(name)
end
end
alias it_should_not_have_link_to_new it_should_not_link_to_new
alias it_should_not_have_button_to_new it_should_not_link_to_new
alias it_should_not_have_button_or_link_to_new it_should_not_link_to_new
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_show</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_have_link_to_show it_should_link_to_show
alias it_should_have_button_to_show it_should_link_to_show
alias it_should_have_button_or_link_to_show it_should_link_to_show
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should_not have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_not_have_link_to_show it_should_not_link_to_show
alias it_should_not_have_button_to_show it_should_not_link_to_show
alias it_should_not_have_button_or_link_to_show it_should_not_link_to_show
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_show_each(name)
it "should have a link/button to show each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_show(member)
end
end
end
alias it_should_have_link_to_show_each it_should_link_to_show_each
alias it_should_have_button_to_show_each it_should_link_to_show_each
alias it_should_have_button_or_link_to_show_each it_should_link_to_show_each
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_edit</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_have_link_to_edit it_should_link_to_edit
alias it_should_have_button_to_edit it_should_link_to_edit
alias it_should_have_button_or_link_to_edit it_should_link_to_edit
# Negative version of <tt>it_should_link_to_edit</tt>. See that method
# for more details.
def it_should_not_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should_not have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_not_have_link_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_or_link_to_edit it_should_not_link_to_edit
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_edit_each(name)
it "should have a link/button to edit each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_edit(member)
end
end
end
alias it_should_have_link_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_or_link_to_edit_each it_should_link_to_edit_each
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_delete</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_delete(name)
it "should have a link/button to delete @#{name}" do
do_render
response.should have_button_to_delete(instance_for(name))
end
end
alias it_should_have_link_to_delete it_should_link_to_delete
alias it_should_have_button_to_delete it_should_link_to_delete
alias it_should_have_button_or_link_to_delete it_should_link_to_delete
# Negative version of <tt>it_should_link_to_delete</tt>. See that method
# for more details.
def it_should_not_link_to_delete(name)
it "should not have a link/button to delete @#{name}" do
do_render
response.should_not have_button_to_delete(instance_for(name))
end
end
alias it_should_not_have_link_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_or_link_to_delete it_should_not_link_to_delete
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_delete_each(name)
it "should have a link/button to delete each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_button_to_delete(member)
end
end
end
alias it_should_have_link_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_or_link_to_delete_each it_should_link_to_delete_each
# Creates an expectation that the template should call <tt>render :partial</tt>
# with the specified template.
def it_should_render_partial(name)
it "should render :partial => '#{name}'" do
template.should_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Negative version of <tt>it_should_render_partial</tt>. See that method
# for more details.
def it_should_not_render_partial(name)
it "should not render :partial => '#{name}'" do
template.should_not_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Sets <tt>@the_template</tt> (for use in <tt>do_render</tt>) using the current
# example group description. Example:
#
# describe "users/index.haml.erb" do
# use_describe_for_template!
# # ...
# end
#
# This is equivalent to setting <tt>@the_template = "users/index.haml.erb"</tt>
# in a before block.
def use_describe_for_template!
template = self_description_text
if File.exists?(File.join(RAILS_ROOT, "app/views", template))
before(:each) do
@the_template = template
end
else
error_message = "You called use_describe_for_template! "
error_message << "but 'app/views/#{template}' does not exist. "
raise NameError, error_message
end
end
end
end
end

View file

@ -0,0 +1,15 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort.
module ViewStubHelpers
# Shorthand for the following stub:
#
# template.stub!(:render).with(hash_including(:partial => anything))
def stub_partial_rendering!
template.stub!(:render).with(hash_including(:partial => anything))
end
end
end

View file

@ -0,0 +1,26 @@
# Let's make sure everyone else is loaded
require File.expand_path(RAILS_ROOT + "/config/environment")
require 'spec'
require 'spec/rails'
begin
require 'ruby2ruby'
rescue LoadError
puts "-----"
puts "Attention: skinny_spec requires ruby2ruby for nicer route descriptions"
puts "It is highly recommended that you install it: sudo gem install ruby2ruby"
puts "-----"
end
# Let's load our family now
require "lucky_sneaks/common_spec_helpers"
require "lucky_sneaks/controller_request_helpers"
require "lucky_sneaks/controller_spec_helpers"
require "lucky_sneaks/controller_stub_helpers"
require "lucky_sneaks/model_spec_helpers"
require "lucky_sneaks/view_spec_helpers"
# Let's all come together
Spec::Rails::Example::ViewExampleGroup.send :include, LuckySneaks::ViewSpecHelpers
Spec::Rails::Example::HelperExampleGroup.send :include, LuckySneaks::CommonSpecHelpers
Spec::Rails::Example::ControllerExampleGroup.send :include, LuckySneaks::ControllerSpecHelpers
Spec::Rails::Example::ModelExampleGroup.send :include, LuckySneaks::ModelSpecHelpers

View file

@ -0,0 +1,46 @@
= swf_fu --- History
== Version 1.4.0 - May 8, 2010
* Any option can be a block, in which case it is called (with the source swf passed as argument)
== Version 1.3.1 - February 5, 2010
* Improved compatibility with Rails 3.0: swf_tag now outputs html_safe content.
* Got rid of deprecation warning in Rails 2.2+ when using swf_tag in block form.
== Version 1.3.0 - June 20, 2009
* Updated to swf_object v2.2. Change should not be noticeable to users, except compatibility improvements and better auto install. Added the option +switch_off_auto_hide_show+.
== Version 1.2.0 - January 14, 2009
* flashvars[:id] will now default to the DOM id of the object. I didn't want to have any extra defaults than the very basic ones, but there is no easy way to get this from Flash (see http://www.actionscript.org/forums/showthread.php3?t=136044 ) and no easy way to specify that using +swf_default_options+.
* If flashvars is a string (e.g. "myVar=myValue") it will be parsed into a hash so that the behaviour for default values apply to strings or hashes. swf_default_options[:flashvars] can also be a string and will also be parsed before being merged.
* Small bug fix: the options passed as hashes (:flashvars, :parameters and :html_options) were changed if swf_default_options[:flashvars, ...] existed. They are now left unchanged.
== Version 1.1.0 - January 3, 2009
* Improved the way to specify alternate content
== Version 1.0.3 - January 3, 2009
* Improved javascript initialization
:initialize => [1, 2, 3] # produces in javascript: obj.initialize(1,2,3) instead of ([1,2,3])
# no :initialize produces in javascript: obj.initialize() instead of (null)
:initialize => nil # stil produces obj.initialize(null)
== Version 1.0.2 - January 3, 2009
* Bug fix for flashvars in dynamic method
== Version 1.0.1 - January 2, 2009
* File reorganization
* Bug fix for default options
== Version 1.0 - X-mas, 2008
=== Initial release.

Some files were not shown because too many files have changed in this diff Show more