Purged git history and removed sensitive information.

This commit is contained in:
Luke Barratt 2014-03-23 00:22:25 +00:00
commit 6bcc8dc76b
862 changed files with 25312 additions and 0 deletions

47
vendor/plugins/acts-as-readable/README vendored Normal file
View file

@ -0,0 +1,47 @@
ActsAsReadable
==============
ActsAsReadable allows you to create a generic relationship of items which can
be marked as 'read' by users. This is useful for forums or any other kind of
situation where you might need to know whether or not a user has seen a particular
model.
Installation
============
To install the plugin just install from the SVN:
script/plugin install http://svn.intridea.com/svn/public/acts_as_readable
You will need the readings table to use this plugin. A generator has been included,
simply type
script/generate acts_as_readable_migration
to get the standard migration created for you.
Example
=======
class Post < ActiveRecord::Base
acts_as_readable
end
bob = User.find_by_name("bob")
bob.readings # => []
Post.find_unread_by(bob) # => [<Post 1>,<Post 2>,<Post 3>...]
Post.find_read_by(bob) # => []
Post.find(1).read_by?(bob) # => false
Post.find(1).read_by!(bob) # => <Reading 1>
Post.find(1).read_by?(bob) # => true
Post.find(1).users_who_read # => [<User bob>]
Post.find_unread_by(bob) # => [<Post 2>,<Post 3>...]
Post.find_read_by(bob) # => [<Post 1>]
bob.readings # => [<Reading 1>]
Copyright (c) 2008 Michael Bleigh and Intridea, Inc. released under the MIT license

View file

@ -0,0 +1,11 @@
class ActsAsReadableMigrationGenerator < Rails::Generator::Base
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
def file_name
"acts_as_readable_migration"
end
end

View file

@ -0,0 +1,14 @@
class ActsAsReadableMigration < ActiveRecord::Migration
def self.up
create_table :readings do |t|
t.string :readable_type
t.integer :readable_id
t.integer :user_id
t.timestamps
end
end
def self.down
drop_table :readings
end
end

View file

@ -0,0 +1,6 @@
require File.join(File.dirname(__FILE__), 'lib', 'acts_as_readable')
require File.join(File.dirname(__FILE__), 'lib', 'reading')
require File.join(File.dirname(__FILE__), 'lib', 'user_with_readings')
ActiveRecord::Base.send :include, ActiveRecord::Acts::Readable

View file

@ -0,0 +1,43 @@
module ActiveRecord
module Acts
module Readable
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_readable
has_many :readings, :as => :readable
has_many :users_who_read, :through => :readings, :source => :user
include ActiveRecord::Acts::Readable::InstanceMethods
extend ActiveRecord::Acts::Readable::SingletonMethods
end
end
module SingletonMethods
def find_unread_by(user)
find(:all) - find_read_by(user)
end
def find_read_by(user)
find(:all, :conditions => ["readings.readable_id = #{table_name}.id AND readings.user_id=?", user.id], :include => :readings)
end
end
module InstanceMethods
def read_by!(user)
readings << Reading.new(:user_id => user.id)
end
def unread_by!(user)
readings.find(:first, :conditions => ["user_id = ?",user.id])
end
def read_by?(user)
!!users_who_read.find(:first, :conditions => ["user_id = ?",user.id])
end
end
end
end
end

View file

@ -0,0 +1,7 @@
class Reading < ActiveRecord::Base
belongs_to :user
belongs_to :readable, :polymorphic => true
validates_presence_of :user_id, :readable_id, :readable_type
validates_uniqueness_of :user_id, :scope => [:readable_id, :readable_type]
end

View file

@ -0,0 +1,3 @@
User.class_eval do
has_many :readings
end

View file

@ -0,0 +1,20 @@
Copyright (c) 2007 [name of plugin creator]
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

40
vendor/plugins/acts_as_rateable/README vendored Normal file
View file

@ -0,0 +1,40 @@
Acts As Rateble
=============
Acts_as_rateable is a plugin released under the MIT license.
It makes activerecord models rateable through a polymorphic association and optionally logs which user rated which model.
In this case, one user can rate an object once. Used on cotcot.hu for article rating, sponsored quizzes, etc.
Example
=======
Install the plugin into your vendor/plugins directory, insert 'acts_as_rateable' into your model, then restart your application.
class Post < ActiveRecord::Base
acts_as_rateable
end
Now your model is extended by the plugin, you can rate it ( 1-# )or calculate the average rating.
@post.rate_it( 4, current_user.id )
@post.average_rating #=> 4.0
@post.average_rating_round #=> 4
@post.average_rating_percent #=> 80
@post.rated_by?( current_user ) #=> rating || false
Post.find_average_of( 4 ) #=> array of posts
See acts_as_rateable.rb for further details!
# Notes
Jinzhu - generator is compatible with rails 3.
Copyright (c) 2007-2010 Ferenc Fekete, http://feketeferenc.hu , 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 acts_as_ratable plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the acts_as_ratable plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ActsAsRatable'
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 ActsAsRateableMigrationGenerator < Rails::Generator::Base
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
def file_name
"acts_as_rateable_migration"
end
end

View file

@ -0,0 +1,23 @@
class ActsAsRateableMigration < ActiveRecord::Migration
def self.up
create_table :rates do |t|
t.column :score, :integer
end
create_table :ratings do |t|
t.column :user_id, :integer
t.column :rate_id, :integer
t.column :rateable_id, :integer
t.column :rateable_type, :string, :limit => 32
t.timestamps
end
add_index :ratings, :rate_id
add_index :ratings, [:rateable_id, :rateable_type]
end
def self.down
drop_table :ratings
drop_table :rates
end
end

View file

@ -0,0 +1,2 @@
require File.dirname(__FILE__) + '/lib/acts_as_rateable'
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Rateable)

View file

@ -0,0 +1,70 @@
module ActiveRecord
module Acts
module Rateable
def self.included(base)
base.extend(ClassMethods)
end
module AssignRateWithUserId
def <<( rate )
r = Rating.new
r.rate = rate
r.rateable = proxy_owner
r.user_id = rate.user_id
r.save
end
end
module ClassMethods
def acts_as_rateable(options = {})
has_many :ratings, :as => :rateable, :dependent => :destroy, :include => :rate
has_many :rates, :through => :ratings, :extend => AssignRateWithUserId
include ActiveRecord::Acts::Rateable::InstanceMethods
extend ActiveRecord::Acts::Rateable::SingletonMethods
end
end
module SingletonMethods
# Find all objects rated by score.
def find_average_of( score )
find(:all, :include => [:rates] ).collect {|i| i if i.average_rating.to_i == score }.compact
end
end
module InstanceMethods
# Rates the object by a given score. A user object can be passed to the method.
def rate_it( score, user_id )
return unless score
rate = Rate.find_or_create_by_score( score.to_i )
rate.user_id = user_id
rates << rate
end
# Calculates the average rating. Calculation based on the already given scores.
def average_rating
return 0 if rates.empty?
( rates.inject(0){|total, rate| total += rate.score }.to_f / rates.size )
end
# Rounds the average rating value.
def average_rating_round
average_rating.round
end
# Returns the average rating in percent. The maximal score must be provided or the default value (5) will be used.
# TODO make maximum_rating automatically calculated.
def average_rating_percent( maximum_rating = 5 )
f = 100 / maximum_rating.to_f
average_rating * f
end
# Checks wheter a user rated the object or not.
def rated_by?( user )
ratings.detect {|r| r.user_id == user.id }
end
end
end
end
end

View file

@ -0,0 +1,17 @@
require 'rails/generators/migration'
class ActsAsRateableMigrationGenerator < Rails::Generators::Base
include Rails::Generators::Migration
def self.source_root
@_acts_as_commentable_source_root ||= File.expand_path("../templates", __FILE__)
end
def self.next_migration_number(path)
Time.now.utc.strftime("%Y%m%d%H%M%S")
end
def copy_migration_file
migration_template 'migration.rb', 'db/migrate/acts_as_rateable_migration'
end
end

View file

@ -0,0 +1,23 @@
class ActsAsRateableMigration < ActiveRecord::Migration
def self.up
create_table :rates do |t|
t.column :score, :integer
end
create_table :ratings do |t|
t.column :user_id, :integer
t.column :rate_id, :integer
t.column :rateable_id, :integer
t.column :rateable_type, :string, :limit => 32
t.timestamps
end
add_index :ratings, :rate_id
add_index :ratings, [:rateable_id, :rateable_type]
end
def self.down
drop_table :ratings
drop_table :rates
end
end

View file

@ -0,0 +1,10 @@
class Rate < ActiveRecord::Base
has_many :ratings
validates_presence_of :score
validates_uniqueness_of :score
validates_numericality_of :score, :greater_than_or_equal_to => 1, :less_than_or_equal_to => 10
attr_accessor :user_id
end

View file

@ -0,0 +1,6 @@
class Rating < ActiveRecord::Base
belongs_to :rate
belongs_to :rateable, :polymorphic => true
validates_uniqueness_of :user_id, :scope => [:rateable_id, :rateable_type]
end

View file

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

View file

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

View file

@ -0,0 +1,5 @@
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE

View file

@ -0,0 +1,84 @@
*GIT* (version numbers are overrated)
* 0.6 (19 Jul 2010) Rails 3 refactoring by gvarela!
* (16 Jun 2008) Backwards Compatibility is overrated (big updates for rails 2.1)
* Use ActiveRecord 2.1's dirty attribute checking instead [Asa Calow]
* Remove last traces of #non_versioned_fields
* Remove AR::Base.find_version and AR::Base.find_versions, rely on AR association proxies and named_scope
* Remove #versions_count, rely on AR association counter caching.
* Remove #versioned_attributes, basically the same as AR::Base.versioned_columns
* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
*0.5.1*
* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
*0.5* # do versions even matter for plugins?
* (21 Apr 2006) Added without_locking and without_revision methods.
Foo.without_revision do
@foo.update_attributes ...
end
*0.4*
* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
*0.3.1*
* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
* (7 Jan 2006) added tests to prove has_many :through joins work
*0.3*
* (2 Jan 2006) added ability to share a mixin with versioned class
* (2 Jan 2006) changed the dynamic version model to MyModel::Version
*0.2.4*
* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
*0.2.3*
* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
* (12 Nov 2005) updated tests to use ActiveRecord Schema
*0.2.2*
* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
*0.2.1*
* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
*0.2*
* (6 Oct 2005) added find_versions and find_version class methods.
* (6 Oct 2005) removed transaction from create_versioned_table().
this way you can specify your own transaction around a group of operations.
* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
*0.1.3* (18 Sep 2005)
* First RubyForge release
*0.1.2*
* check if module is already included when acts_as_versioned is called
*0.1.1*
* Adding tests and rdocs
*0.1*
* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974

View file

@ -0,0 +1,7 @@
source 'http://rubygems.org'
group :development do
gem 'rails', '3.1.0'
gem 'sqlite3-ruby', '1.3.1'
gem 'mysql', '2.8.1'
end

View file

@ -0,0 +1,20 @@
Copyright (c) 2005 Rick Olson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

24
vendor/plugins/acts_as_versioned/README vendored Normal file
View file

@ -0,0 +1,24 @@
= acts_as_versioned
This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
== Resources
Install
* gem install acts_as_versioned
<3 GitHub
* http://github.com/technoweenie/acts_as_versioned
Gemcutter FTW
* http://gemcutter.org/gems/acts_as_versioned
Subversion
* http://svn.github.com/technoweenie/acts_as_versioned.git
Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
was the first project to use acts_as_versioned <em>in the wild</em>.

View file

@ -0,0 +1,41 @@
== Creating the test database
The default name for the test databases is "activerecord_versioned". If you
want to use another database name then be sure to update the connection
adapter setups you want to test with in test/connections/<your database>/connection.rb.
When you have the database online, you can import the fixture tables with
the test/fixtures/db_definitions/*.sql files.
Make sure that you create database objects with the same user that you specified in i
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all the adapters. You can also run the suite on just
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
Unit tests are located in test directory. If you only want to run a single test suite,
or don't want to bother with Rake, you can do so with something like:
cd test; ruby -I "connections/native_mysql" base_test.rb
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
and test suite name as needed.
== Faster tests
If you are using a database that supports transactions, you can set the
"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
This gives a very large speed boost. With rake:
rake AR_TX_FIXTURES=yes
Or, by hand:
AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb

View file

@ -0,0 +1,146 @@
require 'rubygems'
require 'rake'
require 'date'
#############################################################################
#
# Helper functions
#
#############################################################################
def name
@name ||= Dir['*.gemspec'].first.split('.').first
end
def version
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
end
def date
Date.today.to_s
end
def rubyforge_project
name
end
def gemspec_file
"#{name}.gemspec"
end
def gem_file
"#{name}-#{version}.gem"
end
def replace_header(head, header_name)
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
end
#############################################################################
#
# Standard tasks
#
#############################################################################
task :default => :test
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
desc "Generate RCov test coverage and open in your browser"
task :coverage do
require 'rcov'
sh "rm -fr coverage"
sh "rcov test/test_*.rb"
sh "open coverage/index.html"
end
require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "#{name} #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
desc "Open an irb session preloaded with this library"
task :console do
sh "irb -rubygems -r ./lib/#{name}.rb"
end
#############################################################################
#
# Custom tasks (add your own tasks here)
#
#############################################################################
#############################################################################
#
# Packaging tasks
#
#############################################################################
task :release => :build do
unless `git branch` =~ /^\* master$/
puts "You must be on the master branch to release!"
exit!
end
sh "git commit --allow-empty -a -m 'Release #{version}'"
sh "git tag v#{version}"
sh "git push origin master"
sh "git push v#{version}"
sh "gem push pkg/#{name}-#{version}.gem"
end
task :build => :gemspec do
sh "mkdir -p pkg"
sh "gem build #{gemspec_file}"
sh "mv #{gem_file} pkg"
end
task :gemspec => :validate do
# read spec file and split out manifest section
spec = File.read(gemspec_file)
head, manifest, tail = spec.split(" # = MANIFEST =\n")
# replace name version and date
replace_header(head, :name)
replace_header(head, :version)
replace_header(head, :date)
#comment this out if your rubyforge_project has a different name
replace_header(head, :rubyforge_project)
# determine file list from git ls-files
files = `git ls-files`.
split("\n").
sort.
reject { |file| file =~ /^\./ }.
reject { |file| file =~ /^(rdoc|pkg)/ }.
map { |file| " #{file}" }.
join("\n")
# piece file back together and write
manifest = " s.files = %w[\n#{files}\n ]\n"
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
File.open(gemspec_file, 'w') { |io| io.write(spec) }
puts "Updated #{gemspec_file}"
end
task :validate do
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
unless libfiles.empty?
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
exit!
end
unless Dir['VERSION*'].empty?
puts "A `VERSION` file at root level violates Gem best practices."
exit!
end
end

View file

@ -0,0 +1,85 @@
## This is the rakegem gemspec template. Make sure you read and understand
## all of the comments. Some sections require modification, and others can
## be deleted if you don't need them. Once you understand the contents of
## this file, feel free to delete any comments that begin with two hash marks.
## You can find comprehensive Gem::Specification documentation, at
## http://docs.rubygems.org/read/chapter/20
Gem::Specification.new do |s|
s.specification_version = 2 if s.respond_to? :specification_version=
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.rubygems_version = '1.3.5'
## Leave these as is they will be modified for you by the rake gemspec task.
## If your rubyforge_project name is different, then edit it and comment out
## the sub! line in the Rakefile
s.name = 'acts_as_versioned'
s.version = '0.6.0'
s.date = '2010-07-19'
s.rubyforge_project = 'acts_as_versioned'
## Make sure your summary is short. The description may be as long
## as you like.
s.summary = "Add simple versioning to ActiveRecord models."
s.description = "Add simple versioning to ActiveRecord models."
## List the primary authors. If there are a bunch of authors, it's probably
## better to set the email to an email list or something. If you don't have
## a custom homepage, consider using your GitHub URL or the like.
s.authors = ["Rick Olson"]
s.email = 'technoweenie@gmail.com'
s.homepage = 'http://github.com/technoweenie/acts_as_versioned'
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
s.require_paths = %w[lib]
## Specify any RDoc options here. You'll want to add your README and
## LICENSE files to the extra_rdoc_files list.
s.rdoc_options = ["--charset=UTF-8"]
s.extra_rdoc_files = %w[README MIT-LICENSE CHANGELOG]
## List your runtime dependencies here. Runtime dependencies are those
## that are needed for an end user to actually USE your code.
s.add_dependency('activerecord', [">= 3.1.0"])
## List your development dependencies here. Development dependencies are
## those that are only needed during development
s.add_development_dependency('sqlite3-ruby', ["~> 1.3.1"])
## Leave this section as-is. It will be automatically generated from the
## contents of your Git repository via the gemspec task. DO NOT REMOVE
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
# = MANIFEST =
s.files = %w[
CHANGELOG
Gemfile
MIT-LICENSE
README
RUNNING_UNIT_TESTS
Rakefile
acts_as_versioned.gemspec
init.rb
lib/acts_as_versioned.rb
test/abstract_unit.rb
test/database.yml
test/fixtures/authors.yml
test/fixtures/landmark.rb
test/fixtures/landmark_versions.yml
test/fixtures/landmarks.yml
test/fixtures/locked_pages.yml
test/fixtures/locked_pages_revisions.yml
test/fixtures/migrations/1_add_versioned_tables.rb
test/fixtures/page.rb
test/fixtures/page_versions.yml
test/fixtures/pages.yml
test/fixtures/widget.rb
test/migration_test.rb
test/schema.rb
test/versioned_test.rb
]
# = MANIFEST =
## Test files will be grabbed from the file list. Make sure the path glob
## matches what you actually use.
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
end

View file

@ -0,0 +1 @@
require File.join(File.dirname(__FILE__), 'lib', 'acts_as_versioned')

View file

@ -0,0 +1,494 @@
# Copyright (c) 2005 Rick Olson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'active_support/concern'
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
# column is present as well.
#
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
#
# class Page < ActiveRecord::Base
# # assumes pages_versions table
# acts_as_versioned
# end
#
# Example:
#
# page = Page.create(:title => 'hello world!')
# page.version # => 1
#
# page.title = 'hello world'
# page.save
# page.version # => 2
# page.versions.size # => 2
#
# page.revert_to(1) # using version number
# page.title # => 'hello world!'
#
# page.revert_to(page.versions.last) # using versioned instance
# page.title # => 'hello world'
#
# page.versions.earliest # efficient query to find the first version
# page.versions.latest # efficient query to find the most recently created version
#
#
# Simple Queries to page between versions
#
# page.versions.before(version)
# page.versions.after(version)
#
# Access the previous/next versions from the versioned model itself
#
# version = page.versions.latest
# version.previous # go back one version
# version.next # go forward one version
#
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
module Versioned
VERSION = "0.6.0"
CALLBACKS = [:set_new_version, :save_version, :save_version?]
# == Configuration options
#
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
# For finer control, pass either a Proc or modify Model#version_condition_met?
#
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
#
# or...
#
# class Auction
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
# !expired?
# end
# end
#
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
# either a symbol or array of symbols.
#
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
# to create an anonymous mixin:
#
# class Auction
# acts_as_versioned do
# def started?
# !started_at.nil?
# end
# end
# end
#
# or...
#
# module AuctionExtension
# def started?
# !started_at.nil?
# end
# end
# class Auction
# acts_as_versioned :extend => AuctionExtension
# end
#
# Example code:
#
# @auction = Auction.find(1)
# @auction.started?
# @auction.versions.first.started?
#
# == Database Schema
#
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
# into a table called #{model}_versions where the model name is singlular. The _versions table should
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
#
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
# then that field is reflected in the versioned model as 'versioned_type' by default.
#
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
#
# class AddVersions < ActiveRecord::Migration
# def self.up
# # create_versioned_table takes the same options hash
# # that create_table does
# Post.create_versioned_table
# end
#
# def self.down
# Post.drop_versioned_table
# end
# end
#
# == Changing What Fields Are Versioned
#
# By default, acts_as_versioned will version all but these fields:
#
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
#
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
#
# class Post < ActiveRecord::Base
# acts_as_versioned
# self.non_versioned_columns << 'comments_count'
# end
#
def acts_as_versioned(options = {}, &extension)
# don't allow multiple calls
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::Behaviors)
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
:version_association_options, :version_if_changed
self.versioned_class_name = options[:class_name] || "Version"
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
self.version_column = options[:version_column] || 'version'
self.version_sequence_name = options[:sequence_name]
self.max_version_limit = options[:limit].to_i
self.version_condition = options[:if] || true
self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] + options[:non_versioned_columns].to_a.map(&:to_s)
self.version_association_options = {
:class_name => "#{self.to_s}::#{versioned_class_name}",
:foreign_key => versioned_foreign_key,
:dependent => :delete_all
}.merge(options[:association_options] || {})
if block_given?
extension_module_name = "#{versioned_class_name}Extension"
silence_warnings do
self.const_set(extension_module_name, Module.new(&extension))
end
options[:extend] = self.const_get(extension_module_name)
end
unless options[:if_changed].nil?
self.track_altered_attributes = true
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
self.version_if_changed = options[:if_changed].map(&:to_s)
end
include options[:extend] if options[:extend].is_a?(Module)
include ActiveRecord::Acts::Versioned::Behaviors
#
# Create the dynamic versioned model
#
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
def self.reloadable?;
false;
end
# find first version before the given version
def self.before(version)
where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
order('version DESC').
first
end
# find first version after the given version.
def self.after(version)
where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
order('version ASC').
first
end
# finds earliest version of this record
def self.earliest
order("#{original_class.version_column}").first
end
# find latest version of this record
def self.latest
order("#{original_class.version_column} desc").first
end
def previous
self.class.before(self)
end
def next
self.class.after(self)
end
def versions_count
page.version
end
end
versioned_class.cattr_accessor :original_class
versioned_class.original_class = self
versioned_class.set_table_name versioned_table_name
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
:class_name => "::#{self.to_s}",
:foreign_key => versioned_foreign_key
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end
module Behaviors
extend ActiveSupport::Concern
included do
has_many :versions, self.version_association_options
before_save :set_new_version
after_save :save_version
after_save :clear_old_versions
end
module InstanceMethods
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
def save_version
if @saving_version
@saving_version = nil
rev = self.class.versioned_class.new
clone_versioned_model(self, rev)
rev.send("#{self.class.version_column}=", send(self.class.version_column))
rev.send("#{self.class.versioned_foreign_key}=", id)
rev.save
end
end
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
# Override this method to set your own criteria for clearing old versions.
def clear_old_versions
return if self.class.max_version_limit == 0
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
if excess_baggage > 0
self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
end
end
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
def revert_to(version)
if version.is_a?(self.class.versioned_class)
return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
else
return false unless version = versions.where(self.class.version_column => version).first
end
self.clone_versioned_model(version, self)
send("#{self.class.version_column}=", version.send(self.class.version_column))
true
end
# Reverts a model to a given version and saves the model.
# Takes either a version number or an instance of the versioned model
def revert_to!(version)
revert_to(version) ? save_without_revision : false
end
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
def save_without_revision
save_without_revision!
true
rescue
false
end
def save_without_revision!
without_locking do
without_revision do
save!
end
end
end
def altered?
track_altered_attributes ? (version_if_changed - changed).length < version_if_changed.length : changed?
end
# Clones a model. Used when saving a new version or reverting a model's version.
def clone_versioned_model(orig_model, new_model)
self.class.versioned_columns.each do |col|
new_model[col.name] = orig_model.send(col.name) if orig_model.has_attribute?(col.name)
end
if orig_model.is_a?(self.class.versioned_class)
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
elsif new_model.is_a?(self.class.versioned_class)
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
end
end
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
def save_version?
version_condition_met? && altered?
end
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
# custom version condition checking.
def version_condition_met?
case
when version_condition.is_a?(Symbol)
send(version_condition)
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
version_condition.call(self)
else
version_condition
end
end
# Executes the block with the versioning callbacks disabled.
#
# @foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
self.class.without_revision(&block)
end
# Turns off optimistic locking for the duration of the block
#
# @foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
self.class.without_locking(&block)
end
def empty_callback()
end
#:nodoc:
protected
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
def set_new_version
@saving_version = new_record? || save_version?
self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
end
# Gets the next available version for the current record, or 1 for a new record
def next_version
(new_record? ? 0 : versions.calculate(:maximum, version_column).to_i) + 1
end
end
module ClassMethods
# Returns an array of columns that are versioned. See non_versioned_columns
def versioned_columns
@versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
end
# Returns an instance of the dynamic versioned model
def versioned_class
const_get versioned_class_name
end
# Rake migration task to create the versioned table using options passed to acts_as_versioned
def create_versioned_table(create_table_options = {})
# create version column in main table if it does not exist
if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
self.connection.add_column table_name, version_column, :integer
self.reset_column_information
end
return if connection.table_exists?(versioned_table_name)
self.connection.create_table(versioned_table_name, create_table_options) do |t|
t.column versioned_foreign_key, :integer
t.column version_column, :integer
end
self.versioned_columns.each do |col|
self.connection.add_column versioned_table_name, col.name, col.type,
:limit => col.limit,
:default => col.default,
:scale => col.scale,
:precision => col.precision
end
if type_col = self.columns_hash[inheritance_column]
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
:limit => type_col.limit,
:default => type_col.default,
:scale => type_col.scale,
:precision => type_col.precision
end
self.connection.add_index versioned_table_name, versioned_foreign_key
end
# Rake migration task to drop the versioned table
def drop_versioned_table
self.connection.drop_table versioned_table_name
end
# Executes the block with the versioning callbacks disabled.
#
# Foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
class_eval do
CALLBACKS.each do |attr_name|
alias_method "orig_#{attr_name}".to_sym, attr_name
alias_method attr_name, :empty_callback
end
end
block.call
ensure
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, "orig_#{attr_name}".to_sym
end
end
end
# Turns off optimistic locking for the duration of the block
#
# Foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
current = ActiveRecord::Base.lock_optimistically
ActiveRecord::Base.lock_optimistically = false if current
begin
block.call
ensure
ActiveRecord::Base.lock_optimistically = true if current
end
end
end
end
end
end
end
ActiveRecord::Base.extend ActiveRecord::Acts::Versioned

View file

@ -0,0 +1,49 @@
require "rubygems"
require "bundler"
Bundler.setup(:default, :development)
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
require 'active_support'
require 'active_record'
require 'active_record/fixtures'
require 'active_record/test_case'
begin
require 'ruby-debug'
Debugger.start
rescue LoadError
end
require 'acts_as_versioned'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
load(File.dirname(__FILE__) + "/schema.rb")
# set up custom sequence on widget_versions for DBs that support sequences
if ENV['DB'] == 'postgresql'
ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
ActiveRecord::Base.connection.remove_column :widget_versions, :id
ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
end
class ActiveSupport::TestCase #:nodoc:
include ActiveRecord::TestFixtures
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end
$:.unshift(ActiveSupport::TestCase.fixture_path)

View file

@ -0,0 +1,18 @@
sqlite:
adapter: sqlite
dbfile: acts_as_versioned_plugin.sqlite.db
sqlite3:
adapter: sqlite3
database: acts_as_versioned_plugin.sqlite3.db
postgresql:
adapter: postgresql
username: postgres
password: postgres
database: acts_as_versioned_plugin_test
min_messages: ERROR
mysql:
adapter: mysql
host: localhost
username: rails
password:
database: acts_as_versioned_plugin_test

View file

@ -0,0 +1,6 @@
caged:
id: 1
name: caged
mly:
id: 2
name: mly

View file

@ -0,0 +1,3 @@
class Landmark < ActiveRecord::Base
acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
end

View file

@ -0,0 +1,7 @@
washington:
id: 1
landmark_id: 1
version: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667

View file

@ -0,0 +1,7 @@
washington:
id: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667
doesnt_trigger_version: This is not important
version: 1

View file

@ -0,0 +1,10 @@
welcome:
id: 1
title: Welcome to the weblog
lock_version: 24
type: LockedPage
thinking:
id: 2
title: So I was thinking
lock_version: 24
type: SpecialLockedPage

View file

@ -0,0 +1,27 @@
welcome_1:
id: 1
page_id: 1
title: Welcome to the weblg
lock_version: 23
version_type: LockedPage
welcome_2:
id: 2
page_id: 1
title: Welcome to the weblog
lock_version: 24
version_type: LockedPage
thinking_1:
id: 3
page_id: 2
title: So I was thinking!!!
lock_version: 23
version_type: SpecialLockedPage
thinking_2:
id: 4
page_id: 2
title: So I was thinking
lock_version: 24
version_type: SpecialLockedPage

View file

@ -0,0 +1,15 @@
class AddVersionedTables < ActiveRecord::Migration
def self.up
create_table("things") do |t|
t.column :title, :text
t.column :price, :decimal, :precision => 7, :scale => 2
t.column :type, :string
end
Thing.create_versioned_table
end
def self.down
Thing.drop_versioned_table
drop_table "things" rescue nil
end
end

View file

@ -0,0 +1,43 @@
class Page < ActiveRecord::Base
belongs_to :author
has_many :authors, :through => :versions, :order => 'name'
belongs_to :revisor, :class_name => 'Author'
has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
acts_as_versioned :if => :feeling_good? do
def self.included(base)
base.cattr_accessor :feeling_good
base.feeling_good = true
base.belongs_to :author
base.belongs_to :revisor, :class_name => 'Author'
end
def feeling_good?
@@feeling_good == true
end
end
end
module LockedPageExtension
def hello_world
'hello_world'
end
end
class LockedPage < ActiveRecord::Base
acts_as_versioned \
:inheritance_column => :version_type,
:foreign_key => :page_id,
:table_name => :locked_pages_revisions,
:class_name => 'LockedPageRevision',
:version_column => :lock_version,
:limit => 2,
:if_changed => :title,
:extend => LockedPageExtension
end
class SpecialLockedPage < LockedPage
end
class Author < ActiveRecord::Base
has_many :pages
end

View file

@ -0,0 +1,16 @@
welcome_2:
id: 1
page_id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1
welcome_1:
id: 2
page_id: 1
title: Welcome to the weblg
body: Such a lovely day
version: 23
author_id: 2
revisor_id: 2

View file

@ -0,0 +1,8 @@
welcome:
id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1
created_on: "2008-01-01 00:00:00"

View file

@ -0,0 +1,6 @@
class Widget < ActiveRecord::Base
acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
:dependent => :nullify, :order => 'version desc'
}
non_versioned_columns << 'foo'
end

View file

@ -0,0 +1,46 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
if ActiveRecord::Base.connection.supports_migrations?
class Thing < ActiveRecord::Base
attr_accessor :version
acts_as_versioned
end
class MigrationTest < ActiveSupport::TestCase
self.use_transactional_fixtures = false
def teardown
if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
ActiveRecord::Base.connection.initialize_schema_information
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
else
ActiveRecord::Base.connection.initialize_schema_migrations_table
ActiveRecord::Base.connection.assume_migrated_upto_version(0)
end
Thing.connection.drop_table "things" rescue nil
Thing.connection.drop_table "thing_versions" rescue nil
Thing.reset_column_information
end
def test_versioned_migration
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
# take 'er up
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
assert_equal 1, t.versions.size
# check that the price column has remembered its value correctly
assert_equal t.price, t.versions.first.price
assert_equal t.title, t.versions.first.title
assert_equal t[:type], t.versions.first[:type]
# make sure that the precision of the price column has been preserved
assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
# now lets take 'er back down
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
end
end
end

View file

@ -0,0 +1,82 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :pages, :force => true do |t|
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :created_on, :datetime
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
create_table :page_versions, :force => true do |t|
t.column :page_id, :integer
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :created_on, :datetime
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
add_index :page_versions, [:page_id, :version], :unique => true
create_table :authors, :force => true do |t|
t.column :page_id, :integer
t.column :name, :string
end
create_table :locked_pages, :force => true do |t|
t.column :lock_version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :type, :string, :limit => 255
end
create_table :locked_pages_revisions, :force => true do |t|
t.column :page_id, :integer
t.column :lock_version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :version_type, :string, :limit => 255
t.column :updated_at, :datetime
end
add_index :locked_pages_revisions, [:page_id, :lock_version], :unique => true
create_table :widgets, :force => true do |t|
t.column :name, :string, :limit => 50
t.column :foo, :string
t.column :version, :integer
t.column :updated_at, :datetime
end
create_table :widget_versions, :force => true do |t|
t.column :widget_id, :integer
t.column :name, :string, :limit => 50
t.column :version, :integer
t.column :updated_at, :datetime
end
add_index :widget_versions, [:widget_id, :version], :unique => true
create_table :landmarks, :force => true do |t|
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :doesnt_trigger_version,:string
t.column :version, :integer
end
create_table :landmark_versions, :force => true do |t|
t.column :landmark_id, :integer
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :doesnt_trigger_version,:string
t.column :version, :integer
end
add_index :landmark_versions, [:landmark_id, :version], :unique => true
end

View file

@ -0,0 +1,370 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
require File.join(File.dirname(__FILE__), 'fixtures/page')
require File.join(File.dirname(__FILE__), 'fixtures/widget')
class VersionedTest < ActiveSupport::TestCase
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
set_fixture_class :page_versions => Page::Version
def test_saves_versioned_copy
p = Page.create! :title => 'first title', :body => 'first body'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_equal 1, p.version
assert_instance_of Page.versioned_class, p.versions.first
end
def test_saves_without_revision
p = pages(:welcome)
old_versions = p.versions.count
p.save_without_revision
p.without_revision do
p.update_attributes :title => 'changed'
end
assert_equal old_versions, p.versions.count
end
def test_rollback_with_version_number
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(23), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_versioned_class_name
assert_equal 'Version', Page.versioned_class_name
assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
end
def test_versioned_class
assert_equal Page::Version, Page.versioned_class
assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
end
def test_special_methods
assert_nothing_raised { pages(:welcome).feeling_good? }
assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
assert_nothing_raised { locked_pages(:welcome).hello_world }
assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
end
def test_rollback_with_version_class
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(p.versions.find_by_version(23)), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_rollback_fails_with_invalid_revision
p = locked_pages(:welcome)
assert !p.revert_to!(locked_pages(:thinking))
end
def test_saves_versioned_copy_with_options
p = LockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
end
def test_rollback_with_version_number_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 23"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_rollback_with_version_class_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_saves_versioned_copy_with_sti
p = SpecialLockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_rollback_with_version_number_with_sti
p = locked_pages(:thinking)
assert_equal 'So I was thinking', p.title
assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 1"
assert_equal 'So I was thinking!!!', p.title
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_lock_version_works_with_versioning
p = locked_pages(:thinking)
p2 = LockedPage.find(p.id)
p.title = 'fresh title'
p.save
assert_equal 2, p.versions.size # limit!
assert_raises(ActiveRecord::StaleObjectError) do
p2.title = 'stale title'
p2.save
end
end
def test_version_if_condition
p = Page.create! :title => "title"
assert_equal 1, p.version
Page.feeling_good = false
p.save
assert_equal 1, p.version
Page.feeling_good = true
end
def test_version_if_condition2
# set new if condition
Page.class_eval do
def new_feeling_good() title[0..0] == 'a'; end
alias_method :old_feeling_good, :feeling_good?
alias_method :feeling_good?, :new_feeling_good
end
p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'new title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'a title')
assert_equal 2, p.version
assert_equal 2, p.versions.count
# reset original if condition
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
end
def test_version_if_condition_with_block
# set new if condition
old_condition = Page.version_condition
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'a title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'b title')
assert_equal 2, p.version
assert_equal 2, p.versions.count
# reset original if condition
Page.version_condition = old_condition
end
def test_version_no_limit
p = Page.create! :title => "title", :body => 'first body'
p.save
p.save
5.times do |i|
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+2), p.version
end
end
def test_version_max_limit
p = LockedPage.create! :title => "title"
p.update_attributes(:title => "title1")
p.update_attributes(:title => "title2")
5.times do |i|
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+4), p.lock_version
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
end
end
def test_track_altered_attributes_default_value
assert !Page.track_altered_attributes
assert LockedPage.track_altered_attributes
assert SpecialLockedPage.track_altered_attributes
end
def test_track_altered_attributes
p = LockedPage.create! :title => "title"
assert_equal 1, p.lock_version
assert_equal 1, p.versions(true).size
p.body = 'whoa'
assert !p.save_version?
p.save
assert_equal 2, p.lock_version # still increments version because of optimistic locking
assert_equal 1, p.versions(true).size
p.title = 'updated title'
assert p.save_version?
p.save
assert_equal 3, p.lock_version
assert_equal 1, p.versions(true).size # version 1 deleted
p.title = 'updated title!'
assert p.save_version?
p.save
assert_equal 4, p.lock_version
assert_equal 2, p.versions(true).size # version 1 deleted
end
def test_find_versions
assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).size
end
def test_find_version
assert_equal page_versions(:welcome_1), pages(:welcome).versions.find_by_version(23)
end
def test_with_sequence
assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
3.times { Widget.create! :name => 'new widget' }
assert_equal 3, Widget.count
assert_equal 3, Widget.versioned_class.count
end
def test_has_many_through
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
end
def test_has_many_through_with_custom_association
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
end
def test_referential_integrity
pages(:welcome).destroy
assert_equal 0, Page.count
assert_equal 0, Page::Version.count
end
def test_association_options
association = Page.reflect_on_association(:versions)
options = association.options
assert_equal :delete_all, options[:dependent]
association = Widget.reflect_on_association(:versions)
options = association.options
assert_equal :nullify, options[:dependent]
assert_equal 'version desc', options[:order]
assert_equal 'widget_id', options[:foreign_key]
widget = Widget.create! :name => 'new widget'
assert_equal 1, Widget.count
assert_equal 1, Widget.versioned_class.count
widget.destroy
assert_equal 0, Widget.count
assert_equal 1, Widget.versioned_class.count
end
def test_versioned_records_should_belong_to_parent
page = pages(:welcome)
page_version = page.versions.last
assert_equal page, page_version.page
end
def test_unaltered_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
assert !landmarks(:washington).changed?
end
def test_unchanged_string_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
assert !landmarks(:washington).changed?
end
def test_should_find_earliest_version
assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
end
def test_should_find_latest_version
assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
end
def test_should_find_previous_version
assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
end
def test_should_find_next_version
assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
end
def test_should_find_version_count
assert_equal 2, pages(:welcome).versions.size
end
def test_if_changed_creates_version_if_a_listed_column_is_changed
landmarks(:washington).name = "Washington"
assert landmarks(:washington).changed?
assert landmarks(:washington).altered?
end
def test_if_changed_creates_version_if_all_listed_columns_are_changed
landmarks(:washington).name = "Washington"
landmarks(:washington).latitude = 1.0
landmarks(:washington).longitude = 1.0
assert landmarks(:washington).changed?
assert landmarks(:washington).altered?
end
def test_if_changed_does_not_create_new_version_if_unlisted_column_is_changed
landmarks(:washington).doesnt_trigger_version = "This should not trigger version"
assert landmarks(:washington).changed?
assert !landmarks(:washington).altered?
end
def test_without_locking_temporarily_disables_optimistic_locking
enabled1 = false
block_called = false
ActiveRecord::Base.lock_optimistically = true
LockedPage.without_locking do
enabled1 = ActiveRecord::Base.lock_optimistically
block_called = true
end
enabled2 = ActiveRecord::Base.lock_optimistically
assert block_called
assert !enabled1
assert enabled2
end
def test_without_locking_reverts_optimistic_locking_settings_if_block_raises_exception
assert_raises(RuntimeError) do
LockedPage.without_locking do
raise RuntimeError, "oh noes"
end
end
assert ActiveRecord::Base.lock_optimistically
end
end

View file

@ -0,0 +1,2 @@
coverage
rdoc

View file

@ -0,0 +1,20 @@
Copyright (c) 2008 Adam Meehan
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,9 @@
== Country Code Select
A simple country code select helper. Works exactly the same as country_select but uses country codes instead.
country_code_select(:user, :country, [[ 'US', 'United States' ], [ 'CA', 'Canada' ]])
== Copyright/License
Copyright (c) 2008 Russ Smith, released under the MIT license.

View file

@ -0,0 +1,9 @@
== Country Code Select
A simple country code select helper. Works exactly the same as country_select but uses country codes instead.
country_code_select(:user, :country, [[ 'US', 'United States' ], [ 'CA', 'Canada' ]])
== Copyright/License
Copyright (c) 2008 Russ Smith, released under the MIT license.

View file

@ -0,0 +1,31 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'spec/rake/spectask'
desc 'Default: run specs'
task :default => :spec
spec_files = Rake::FileList["spec/**/*_spec.rb"]
desc "Run specs"
Spec::Rake::SpecTask.new do |t|
t.spec_files = spec_files
t.spec_opts = ["-c"]
end
desc "Generate code coverage"
Spec::Rake::SpecTask.new(:coverage) do |t|
t.spec_files = spec_files
t.rcov = true
t.rcov_opts = ['--exclude', 'spec,/var/lib/gems']
end
desc 'Generate documentation for the country_code_select plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'CountryCodeSelect'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1 @@
require 'country_code_select'

View file

@ -0,0 +1,8 @@
require 'country_code_select/countries'
require 'country_code_select/form_builder'
require 'country_code_select/form_helpers'
require 'country_code_select/instance_tag'
ActionView::Base.send(:include, CountryCodeSelect::FormHelpers)
ActionView::Helpers::InstanceTag.send(:include, CountryCodeSelect::InstanceTag)
ActionView::Helpers::FormBuilder.send(:include, CountryCodeSelect::FormBuilder)

View file

@ -0,0 +1,42 @@
module CountryCodeSelect
module Countries
COUNTRIES = [["Afghanistan", "AF"], ["Albania", "AL"], ["Algeria", "DZ"], ["American Samoa", "AS"], ["Andorra", "AD"], ["Angola", "AO"],
["Anguilla", "AI"], ["Antarctica", "AQ"], ["Antigua and Barbuda", "AG"], ["Argentina", "AR"], ["Armenia", "AM"], ["Aruba", "AW"],
["Australia", "AU"], ["Austria", "AT"], ["Azerbaidjan", "AZ"], ["Bahamas", "BS"], ["Bahrain", "BH"], ["Banglades", "BD"], ["Barbados", "BB"],
["Belarus", "BY"], ["Belgium", "BE"], ["Belize", "BZ"], ["Benin", "BJ"], ["Bermuda", "BM"], ["Bolivia", "BO"], ["Bosnia-Herzegovina", "BA"],
["Botswana", "BW"], ["Bouvet Island", "BV"], ["Brazil", "BR"], ["British Indian O. Terr.", "IO"], ["Brunei Darussalam", "BN"], ["Bulgaria", "BG"],
["Burkina Faso", "BF"], ["Burundi", "BI"], ["Buthan", "BT"], ["Cambodia", "KH"], ["Cameroon", "CM"], ["Canada", "CA"], ["Cape Verde", "CV"],
["Cayman Islands", "KY"], ["Central African Rep.", "CF"], ["Chad", "TD"], ["Chile", "CL"], ["China", "CN"], ["Christmas Island", "CX"],
["Cocos (Keeling) Isl.", "CC"], ["Colombia", "CO"], ["Comoros", "KM"], ["Congo", "CG"], ["Cook Islands", "CK"], ["Costa Rica", "CR"],
["Croatia", "HR"], ["Cuba", "CU"], ["Cyprus", "CY"], ["Czech Republic", "CZ"], ["Czechoslovakia", "CS"], ["Denmark", "DK"], ["Djibouti", "DJ"],
["Dominica", "DM"], ["Dominican Republic", "DO"], ["East Timor", "TP"], ["Ecuador", "EC"], ["Egypt", "EG"], ["El Salvador", "SV"],
["Equatorial Guinea", "GQ"], ["Estonia", "EE"], ["Ethiopia", "ET"], ["European Union", "EU"], ["Falkland Isl.(UK)", "FK"], ["Faroe Islands", "FO"], ["Fiji", "FJ"],
["Finland", "FI"], ["France", "FR"], ["France (European Ter.)", "FX"], ["French Southern Terr.", "TF"], ["Gabon", "GA"], ["Gambia", "GM"],
["Georgia", "GE"], ["Germany", "DE"], ["Ghana", "GH"], ["Gibraltar", "GI"], ["Great Britain (UK)", "GB"], ["Greece", "GR"], ["Greenland", "GL"],
["Grenada", "GD"], ["Guadeloupe (Fr.)", "GP"], ["Guam (US)", "GU"], ["Guatemala", "GT"], ["Guinea", "GN"], ["Guinea Bissau", "GW"],
["Guyana", "GY"], ["Guyana (Fr.)", "GF"], ["Haiti", "HT"], ["Heard & McDonald Isl.", "HM"], ["Honduras", "HN"], ["Hong Kong", "HK"],
["Hungary", "HU"], ["Iceland", "IS"], ["India", "IN"], ["Indonesia", "ID"], ["Iran", "IR"], ["Iraq", "IQ"], ["Ireland", "IE"], ["Israel", "IL"],
["Italy", "IT"], ["Ivory Coast", "CI"], ["Jamaica", "JM"], ["Japan", "JP"], ["Jordan", "JO"], ["Kazachstan", "KZ"], ["Kenya", "KE"],
["Kirgistan", "KG"], ["Kiribati", "KI"], ["Korea (North)", "KP"], ["Korea (South)", "KR"], ["Kuwait", "KW"], ["Laos", "LA"], ["Latvia", "LV"],
["Lebanon", "LB"], ["Lesotho", "LS"], ["Liberia", "LR"], ["Libya", "LY"], ["Liechtenstein", "LI"], ["Lithuania", "LT"], ["Luxembourg", "LU"],
["Macau", "MO"], ["Madagascar", "MG"], ["Malawi", "MW"], ["Malaysia", "MY"], ["Maldives", "MV"], ["Mali", "ML"], ["Malta", "MT"],
["Marshall Islands", "MH"], ["Martinique (Fr.)", "MQ"], ["Mauritania", "MR"], ["Mauritius", "MU"], ["Mexico", "MX"], ["Micronesia", "FM"],
["Moldavia", "MD"], ["Monaco", "MC"], ["Mongolia", "MN"], ["Montserrat", "MS"], ["Morocco", "MA"], ["Mozambique", "MZ"], ["Myanmar", "MM"],
["Namibia", "NA"], ["Nauru", "NR"], ["Nepal", "NP"], ["Netherland Antilles", "AN"], ["Netherlands", "NL"], ["Neutral Zone", "NT"],
["New Caledonia (Fr.)", "NC"], ["New Zealand", "NZ"], ["Nicaragua", "NI"], ["Niger", "NE"], ["Nigeria", "NG"], ["Niue", "NU"],
["Norfolk Island", "NF"], ["Northern Mariana Isl.", "MP"], ["Norway", "NO"], ["Oman", "OM"], ["Pakistan", "PK"], ["Palau", "PW"],
["Panama", "PA"], ["Papua New", "PG"], ["Paraguay", "PY"], ["Peru", "PE"], ["Philippines", "PH"], ["Pitcairn", "PN"], ["Poland", "PL"],
["Polynesia (Fr.)", "PF"], ["Portugal", "PT"], ["Puerto Rico (US)", "PR"], ["Qatar", "QA"], ["Reunion (Fr.)", "RE"], ["Romania", "RO"],
["Russian Federation", "RU"], ["Rwanda", "RW"], ["Saint Lucia", "LC"], ["Samoa", "WS"], ["San Marino", "SM"], ["Saudi Arabia", "SA"],
["Senegal", "SN"], ["Seychelles", "SC"], ["Sierra Leone", "SL"], ["Singapore", "SG"], ["Slovak Republic", "SK"], ["Slovenia", "SI"],
["Solomon Islands", "SB"], ["Somalia", "SO"], ["South Africa", "ZA"], ["Soviet Union", "SU"], ["Spain", "ES"], ["Sri Lanka", "LK"],
["St. Helena", "SH"], ["St. Pierre & Miquelon", "PM"], ["St. Tome and Principe", "ST"], ["St.Kitts Nevis Anguilla", "KN"],
["St.Vincent & Grenadines", "VC"], ["Sudan", "SD"], ["Suriname", "SR"], ["Svalbard & Jan Mayen Is", "SJ"], ["Swaziland", "SZ"], ["Sweden", "SE"],
["Switzerland", "CH"], ["Syria", "SY"], ["Tadjikistan", "TJ"], ["Taiwan", "TW"], ["Tanzania", "TZ"], ["Thailand", "TH"], ["Togo", "TG"],
["Tokelau", "TK"], ["Tonga", "TO"], ["Trinidad & Tobago", "TT"], ["Tunisia", "TN"], ["Turkey", "TR"], ["Turkmenistan", "TM"],
["Turks & Caicos Islands", "TC"], ["Tuvalu", "TV"], ["Uganda", "UG"], ["Ukraine", "UA"], ["United Arab Emirates", "AE"], ["United Kingdom", "GB"],
["United States", "US"], ["Uruguay", "UY"], ["US Minor outlying Isl.", "UM"], ["Uzbekistan", "UZ"], ["Vanuatu", "VU"], ["Vatican City State", "VA"],
["Venezuela", "VE"], ["Vietnam", "VN"], ["Virgin Islands (British)", "VG"], ["Virgin Islands (US)", "VI"], ["Wallis & Futuna Islands", "WF"],
["Western Sahara", "EH"], ["Yemen", "YE"], ["Yugoslavia", "YU"], ["Zaire", "ZR"], ["Zambia", "ZM"], ["Zimbabwe", "ZW"]]
end
end

View file

@ -0,0 +1,7 @@
module CountryCodeSelect
module FormBuilder
def country_code_select(method, priority_countries = nil, options= {})
@template.country_code_select(@object_name, method, priority_countries, options.merge(:object => @object))
end
end
end

View file

@ -0,0 +1,7 @@
module CountryCodeSelect
module FormHelpers
def country_code_select(object_name, method, priority_countries = nil, options = {})
ActionView::Helpers::InstanceTag.new(object_name, method, self, options.delete(:object)).to_country_code_select_tag(priority_countries, options)
end
end
end

View file

@ -0,0 +1,27 @@
module CountryCodeSelect
module InstanceTag
include Countries
def to_country_code_select_tag(priority_countries, options = {})
country_code_select(priority_countries, options)
end
# Adapted from Rails country_select. Just uses country codes instead of full names.
def country_code_select(priority_countries, options)
selected = object.send(@method_name)
countries = ''
if priority_countries
countries += options_for_select(priority_countries, selected)
countries += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
end
countries = countries + options_for_select(COUNTRIES, selected)
if Rails::VERSION::STRING.to_f < 3
content_tag(:select, countries, options.merge(:id => "#{@object_name}_#{@method_name}", :name => "#{@object_name}[#{@method_name}]"))
else
content_tag(:select, countries, options.merge(:id => "#{@object_name}_#{@method_name}", :name => "#{@object_name}[#{@method_name}]"), false)
end
end
end
end

View file

@ -0,0 +1,7 @@
require File.dirname(__FILE__) + '/spec_helper'
describe CountryCodeSelect::FormHelpers do
it "should include country_code_select method" do
ActionView::Helpers::FormBuilder.instance_methods.should include('country_code_select')
end
end

View file

@ -0,0 +1,26 @@
require File.dirname(__FILE__) + '/spec_helper'
describe CountryCodeSelect::FormHelpers do
describe "country_code_select" do
include CountryCodeSelect::FormHelpers
before(:each) do
@user = mock('User', :country => nil)
end
it "should output a select field with countries" do
output = country_code_select(:user, :country)
output.should have_tag('select[id=user_country]')
end
it "should output a select field with priority countries" do
output = country_code_select(:user, :country, [ 'US', 'United States' ])
output.should have_tag('option[value=US]')
end
it "should output a select field with passed attributes" do
output = country_code_select(:user, :country, [ 'US', 'United States' ], :class => 'custom_class')
output.should have_tag('select[class=custom_class]')
end
end
end

View file

@ -0,0 +1,31 @@
====================================================================
== RSpec
Copyright (c) 2005-2007 The RSpec Development Team
====================================================================
== ARTS
Copyright (c) 2006 Kevin Clark, Jake Howerton
====================================================================
== ZenTest
Copyright (c) 2001-2006 Ryan Davis, Eric Hodel, Zen Spider Software
====================================================================
== AssertSelect
Copyright (c) 2006 Assaf Arkin
====================================================================
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,130 @@
# This is a wrapper of assert_select for rspec.
module Spec # :nodoc:
module Rails
module Matchers
class AssertSelect #:nodoc:
def initialize(assertion, spec_scope, *args, &block)
@assertion = assertion
@spec_scope = spec_scope
@args = args
@block = block
end
def matches?(response_or_text, &block)
if ActionController::TestResponse === response_or_text and
response_or_text.headers.key?('Content-Type') and
response_or_text.headers['Content-Type'].to_sym == :xml
@args.unshift(HTML::Document.new(response_or_text.body, false, true).root)
elsif String === response_or_text
@args.unshift(HTML::Document.new(response_or_text).root)
end
@block = block if block
begin
@spec_scope.send(@assertion, *@args, &@block)
rescue ::Test::Unit::AssertionFailedError => @error
end
@error.nil?
end
def failure_message; @error.message; end
def negative_failure_message; "should not #{description}, but did"; end
def description
{
:assert_select => "have tag#{format_args(*@args)}",
:assert_select_email => "send email#{format_args(*@args)}",
}[@assertion]
end
private
def format_args(*args)
return "" if args.empty?
return "(#{arg_list(*args)})"
end
def arg_list(*args)
args.collect do |arg|
arg.respond_to?(:description) ? arg.description : arg.inspect
end.join(", ")
end
end
# :call-seq:
# response.should have_tag(*args, &block)
# string.should have_tag(*args, &block)
#
# wrapper for assert_select with additional support for using
# css selectors to set expectation on Strings. Use this in
# helper specs, for example, to set expectations on the results
# of helper methods.
#
# == Examples
#
# # in a controller spec
# response.should have_tag("div", "some text")
#
# # in a helper spec (person_address_tag is a method in the helper)
# person_address_tag.should have_tag("input#person_address")
#
# see documentation for assert_select at http://api.rubyonrails.org/
def have_tag(*args, &block)
AssertSelect.new(:assert_select, self, *args, &block)
end
# wrapper for a nested assert_select
#
# response.should have_tag("div#form") do
# with_tag("input#person_name[name=?]", "person[name]")
# end
#
# see documentation for assert_select at http://api.rubyonrails.org/
def with_tag(*args, &block)
should have_tag(*args, &block)
end
# wrapper for a nested assert_select with false
#
# response.should have_tag("div#1") do
# without_tag("span", "some text that shouldn't be there")
# end
#
# see documentation for assert_select at http://api.rubyonrails.org/
def without_tag(*args, &block)
should_not have_tag(*args, &block)
end
# :call-seq:
# response.should have_rjs(*args, &block)
#
# wrapper for assert_select_rjs
#
# see documentation for assert_select_rjs at http://api.rubyonrails.org/
def have_rjs(*args, &block)
AssertSelect.new(:assert_select_rjs, self, *args, &block)
end
# :call-seq:
# response.should send_email(*args, &block)
#
# wrapper for assert_select_email
#
# see documentation for assert_select_email at http://api.rubyonrails.org/
def send_email(*args, &block)
AssertSelect.new(:assert_select_email, self, *args, &block)
end
# wrapper for assert_select_encoded
#
# see documentation for assert_select_encoded at http://api.rubyonrails.org/
def with_encoded(*args, &block)
should AssertSelect.new(:assert_select_encoded, self, *args, &block)
end
end
end
end

View file

@ -0,0 +1 @@
require 'rspec-rails/assert_select'

View file

@ -0,0 +1,6 @@
--colour
--format
progress
--loadby
mtime
--reverse

View file

@ -0,0 +1,16 @@
$: << File.dirname(__FILE__) + '/../lib' << File.dirname(__FILE__)
require 'rubygems'
require 'spec'
require 'spec/interop/test'
require 'active_support'
require 'action_controller'
require 'action_controller/test_process'
require 'action_view'
require 'rspec-rails/rspec-rails'
require 'country_code_select'
Spec::Runner.configure do |config|
config.include Spec::Rails::Matchers
end

20
vendor/plugins/dynamic_form/MIT-LICENSE vendored Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2010 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

13
vendor/plugins/dynamic_form/README vendored Normal file
View file

@ -0,0 +1,13 @@
DynamicForm
===========
DynamicForm holds a few helpers method to help you deal with your models, they are:
* input(record, method, options = {})
* form(record, options = {})
* error_message_on(object, method, options={})
* error_messages_for(record, options={})
It also adds f.error_messages and f.error_messages_on to your form builders.
Copyright (c) 2010 David Heinemeier Hansson, released under the MIT license

10
vendor/plugins/dynamic_form/Rakefile vendored Normal file
View file

@ -0,0 +1,10 @@
require 'rake/testtask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the active_model_helper plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
end

View file

@ -0,0 +1,12 @@
Gem::Specification.new do |s|
s.name = 'dynamic_form'
s.version = '1.0.0'
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
s.summary = 'Deprecated dynamic form helpers: input, form, error_messages_for, error_messages_on'
s.add_dependency('rails', '>= 3.0.0')
s.files = Dir['lib/**/*']
s.require_path = 'lib'
end

1
vendor/plugins/dynamic_form/init.rb vendored Normal file
View file

@ -0,0 +1 @@
require 'dynamic_form'

View file

@ -0,0 +1,300 @@
require 'action_view/helpers'
require 'active_support/i18n'
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/object/blank'
module ActionView
module Helpers
# The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the +form+
# method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
# is a great way of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form.
# In that case, it's better to use the +input+ method and the specialized +form+ methods in link:classes/ActionView/Helpers/FormHelper.html
module DynamicForm
# Returns a default input tag for the type of object returned by the method. For example, if <tt>@post</tt>
# has an attribute +title+ mapped to a +VARCHAR+ column that holds "Hello World":
#
# input("post", "title")
# # => <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
def input(record_name, method, options = {})
InstanceTag.new(record_name, method, self).to_tag(options)
end
# Returns an entire form with all needed input tags for a specified Active Record object. For example, if <tt>@post</tt>
# has attributes named +title+ of type +VARCHAR+ and +body+ of type +TEXT+ then
#
# form("post")
#
# would yield a form like the following (modulus formatting):
#
# <form action='/posts/create' method='post'>
# <p>
# <label for="post_title">Title</label><br />
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
# </p>
# <p>
# <label for="post_body">Body</label><br />
# <textarea cols="40" id="post_body" name="post[body]" rows="20"></textarea>
# </p>
# <input name="commit" type="submit" value="Create" />
# </form>
#
# It's possible to specialize the form builder by using a different action name and by supplying another
# block renderer. For example, if <tt>@entry</tt> has an attribute +message+ of type +VARCHAR+ then
#
# form("entry",
# :action => "sign",
# :input_block => Proc.new { |record, column|
# "#{column.human_name}: #{input(record, column.name)}<br />"
# })
#
# would yield a form like the following (modulus formatting):
#
# <form action="/entries/sign" method="post">
# Message:
# <input id="entry_message" name="entry[message]" size="30" type="text" /><br />
# <input name="commit" type="submit" value="Sign" />
# </form>
#
# It's also possible to add additional content to the form by giving it a block, such as:
#
# form("entry", :action => "sign") do |form|
# form << content_tag("b", "Department")
# form << collection_select("department", "id", @departments, "id", "name")
# end
#
# The following options are available:
#
# * <tt>:action</tt> - The action used when submitting the form (default: +create+ if a new record, otherwise +update+).
# * <tt>:input_block</tt> - Specialize the output using a different block, see above.
# * <tt>:method</tt> - The method used when submitting the form (default: +post+).
# * <tt>:multipart</tt> - Whether to change the enctype of the form to "multipart/form-data", used when uploading a file (default: +false+).
# * <tt>:submit_value</tt> - The text of the submit button (default: "Create" if a new record, otherwise "Update").
def form(record_name, options = {})
record = instance_variable_get("@#{record_name}")
record = convert_to_model(record)
options = options.symbolize_keys
options[:action] ||= record.persisted? ? "update" : "create"
action = url_for(:action => options[:action], :id => record)
submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize
contents = form_tag({:action => action}, :method =>(options[:method] || 'post'), :enctype => options[:multipart] ? 'multipart/form-data': nil)
contents.safe_concat hidden_field(record_name, :id) if record.persisted?
contents.safe_concat all_input_tags(record, record_name, options)
yield contents if block_given?
contents.safe_concat submit_tag(submit_value)
contents.safe_concat('</form>')
end
# Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
# This error message is wrapped in a <tt>DIV</tt> tag by default or with <tt>:html_tag</tt> if specified,
# which can be extended to include a <tt>:prepend_text</tt> and/or <tt>:append_text</tt> (to properly explain
# the error), and a <tt>:css_class</tt> to style it accordingly. +object+ should either be the name of an
# instance variable or the actual object. The method can be passed in either as a string or a symbol.
# As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
#
# <%= error_message_on "post", "title" %>
# # => <div class="formError">can't be empty</div>
#
# <%= error_message_on @post, :title %>
# # => <div class="formError">can't be empty</div>
#
# <%= error_message_on "post", "title",
# :prepend_text => "Title simply ",
# :append_text => " (or it won't work).",
# :html_tag => "span",
# :css_class => "inputError" %>
# # => <span class="inputError">Title simply can't be empty (or it won't work).</span>
def error_message_on(object, method, *args)
options = args.extract_options!
unless args.empty?
ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' +
'prepend_text, append_text, html_tag, and css_class arguments', caller)
options[:prepend_text] = args[0] || ''
options[:append_text] = args[1] || ''
options[:html_tag] = args[2] || 'div'
options[:css_class] = args[3] || 'formError'
end
options.reverse_merge!(:prepend_text => '', :append_text => '', :html_tag => 'div', :css_class => 'formError')
object = convert_to_model(object)
if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
(errors = obj.errors[method]).presence
content_tag(options[:html_tag],
(options[:prepend_text].html_safe << errors.first).safe_concat(options[:append_text]),
:class => options[:css_class]
)
else
''
end
end
# Returns a string with a <tt>DIV</tt> containing all of the error messages for the objects located as instance variables by the names
# given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are
# provided.
#
# This <tt>DIV</tt> can be tailored by the following options:
#
# * <tt>:header_tag</tt> - Used for the header of the error div (default: "h2").
# * <tt>:id</tt> - The id of the error div (default: "errorExplanation").
# * <tt>:class</tt> - The class of the error div (default: "errorExplanation").
# * <tt>:object</tt> - The object (or array of objects) for which to display errors,
# if you need to escape the instance variable convention.
# * <tt>:object_name</tt> - The object name to use in the header, or any text that you prefer.
# If <tt>:object_name</tt> is not set, the name of the first object will be used.
# * <tt>:header_message</tt> - The message in the header of the error div. Pass +nil+
# or an empty string to avoid the header message altogether. (Default: "X errors
# prohibited this object from being saved").
# * <tt>:message</tt> - The explanation message after the header message and before
# the error list. Pass +nil+ or an empty string to avoid the explanation message
# altogether. (Default: "There were problems with the following fields:").
#
# To specify the display for one object, you simply provide its name as a parameter.
# For example, for the <tt>@user</tt> model:
#
# error_messages_for 'user'
#
# You can also supply an object:
#
# error_messages_for @user
#
# This will use the last part of the model name in the presentation. For instance, if
# this is a MyKlass::User object, this will use "user" as the name in the String. This
# is taken from MyKlass::User.model_name.human, which can be overridden.
#
# To specify more than one object, you simply list them; optionally, you can add an extra <tt>:object_name</tt> parameter, which
# will be the name used in the header message:
#
# error_messages_for 'user_common', 'user', :object_name => 'user'
#
# You can also use a number of objects, which will have the same naming semantics
# as a single object.
#
# error_messages_for @user, @post
#
# If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> parameter which gives the actual
# object (or array of objects to use):
#
# error_messages_for 'user', :object => @question.user
#
# NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what
# you need is significantly different from the default presentation, it makes plenty of sense to access the <tt>object.errors</tt>
# instance yourself and set it up. View the source of this method to see how easy it is.
def error_messages_for(*params)
options = params.extract_options!.symbolize_keys
objects = Array.wrap(options.delete(:object) || params).map do |object|
object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model)
object = convert_to_model(object)
if object.class.respond_to?(:model_name)
options[:object_name] ||= object.class.model_name.human.downcase
end
object
end
objects.compact!
count = objects.inject(0) {|sum, object| sum + object.errors.count }
unless count.zero?
html = {}
[:id, :class].each do |key|
if options.include?(key)
value = options[key]
html[key] = value unless value.blank?
else
html[key] = 'errorExplanation'
end
end
options[:object_name] ||= params.first
I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale|
header_message = if options.include?(:header_message)
options[:header_message]
else
locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ')
end
message = options.include?(:message) ? options[:message] : locale.t(:body)
error_messages = objects.sum do |object|
object.errors.full_messages.map do |msg|
content_tag(:li, msg)
end
end.join.html_safe
contents = ''
contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
contents << content_tag(:p, message) unless message.blank?
contents << content_tag(:ul, error_messages)
content_tag(:div, contents.html_safe, html)
end
else
''
end
end
private
def all_input_tags(record, record_name, options)
input_block = options[:input_block] || default_input_block
record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
end
def default_input_block
Proc.new { |record, column| %(<p><label for="#{record}_#{column.name}">#{column.human_name}</label><br />#{input(record, column.name)}</p>) }
end
module InstanceTagMethods
def to_tag(options = {})
case column_type
when :string
field_type = @method_name.include?("password") ? "password" : "text"
to_input_field_tag(field_type, options)
when :text
to_text_area_tag(options)
when :integer, :float, :decimal
to_input_field_tag("text", options)
when :date
to_date_select_tag(options)
when :datetime, :timestamp
to_datetime_select_tag(options)
when :time
to_time_select_tag(options)
when :boolean
to_boolean_select_tag(options)
end
end
def column_type
object.send(:column_for_attribute, @method_name).type
end
end
module FormBuilderMethods
def error_message_on(method, *args)
@template.error_message_on(@object || @object_name, method, *args)
end
def error_messages(options = {})
@template.error_messages_for(@object_name, objectify_options(options))
end
end
end
class InstanceTag
include DynamicForm::InstanceTagMethods
end
class FormBuilder
include DynamicForm::FormBuilderMethods
end
end
end
I18n.load_path << File.expand_path("../../locale/en.yml", __FILE__)

View file

@ -0,0 +1,8 @@
en:
errors:
template:
header:
one: "1 error prohibited this %{model} from being saved"
other: "%{count} errors prohibited this %{model} from being saved"
# The variable :count is also available
body: "There were problems with the following fields:"

View file

@ -0,0 +1,5 @@
require 'action_view/helpers/dynamic_form'
class ActionView::Base
include DynamicForm
end

View file

@ -0,0 +1,42 @@
require 'test_helper'
class DynamicFormI18nTest < Test::Unit::TestCase
include ActionView::Context
include ActionView::Helpers::DynamicForm
attr_reader :request
def setup
@object = stub :errors => stub(:count => 1, :full_messages => ['full_messages'])
@object.stubs :to_model => @object
@object.stubs :class => stub(:model_name => stub(:human => ""))
@object_name = 'book_seller'
@object_name_without_underscore = 'book seller'
stubs(:content_tag).returns 'content_tag'
I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved"
I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:'
end
def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message
I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').never
error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en')
end
def test_error_messages_for_given_no_header_option_it_translates_header_message
I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns 'header message'
error_messages_for(:object => @object, :locale => 'en')
end
def test_error_messages_for_given_a_message_option_it_does_not_translate_message
I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).never
error_messages_for(:object => @object, :message => 'message', :locale => 'en')
end
def test_error_messages_for_given_no_message_option_it_translates_message
I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:'
error_messages_for(:object => @object, :locale => 'en')
end
end

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
require 'rubygems'
require 'test/unit'
require 'active_support'
require 'active_support/core_ext'
require 'action_view'
require 'action_controller'
require 'action_controller/test_case'
require 'active_model'
require 'action_view/helpers/dynamic_form'

View file

@ -0,0 +1,20 @@
Copyright (c) 2009 Citrus Media Group / Spencer Steffen
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,53 @@
h1. HasViewCount
h3. Adds a polymorphic association that allows any model to have a unique visitor count (based on IP address)
h2. Installation
<pre><code>script/plugin install git://github.com/citrus/has_view_count.git
- or -
git submodule add git://github.com/citrus/has_view_count.git vendor/plugins/has_view_count
script/generate has_view_count
rake db:migrate
restart mongrel
</code></pre>
_This will generate_
<pre><code>db/migrate/XXXXXXXXXXXXXX_has_view_count_migration.rb
app/models/view_count.rb
</code></pre>
***************************************************************************
h2. Example
h4. Model:
<pre><code>class Post < ActiveRecord::Base
has_view_count
end
</code></pre>
h4. Controller:
<pre><code>class PostsController < ApplicationController
after_filter :record_view_count, :only => :show
...
...
...
private
def record_view_count
@post.record_view_count(request.remote_ip, logged_in?) # use logged_in? if you have restful_authentication installed
end
end
</code></pre>
_Copyright (c) 2009 Citrus Media Group / Spencer Steffen, released under the MIT license_

View file

@ -0,0 +1,12 @@
class HasViewCountGenerator < Rails::Generator::Base
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
m.template 'model.rb', 'app/models/view_count.rb'
end
end
def file_name
"has_view_count_migration"
end
end

View file

@ -0,0 +1,14 @@
class HasViewCountMigration < ActiveRecord::Migration
def self.up
create_table :view_counts do |t|
t.references :viewable, :polymorphic => true
t.string :ip_address
t.boolean :logged_in
t.datetime :created_at
end
end
def self.down
drop_table :view_counts
end
end

View file

@ -0,0 +1,10 @@
class ViewCount < ActiveRecord::Base
belongs_to :viewable, :polymorphic => true
validates_uniqueness_of :ip_address, :scope => [ :viewable_id, :viewable_type, :viewed_on ]
def viewed_on
self.created_at.srftime('%x')
end
end

View file

@ -0,0 +1,10 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{has_view_count}
s.version = "0.8.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Citrus Media Group << Spencer Steffen"]
s.date = %q{2009-07-08}
s.description = %q{This plugin makes for easy page view counting.}
end

1
vendor/plugins/has_view_count/init.rb vendored Normal file
View file

@ -0,0 +1 @@
require File.join(Rails.root, 'vendor', 'plugins', 'has_view_count', 'lib', 'has_view_count.rb')

View file

@ -0,0 +1,37 @@
module Citrus
module HasViewCount
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def has_view_count
has_many :view_counts, :as => :viewable, :dependent => :destroy
include Citrus::HasViewCount::InstanceMethods
end
end
module InstanceMethods
def record_view_count(ip_address, logged_in = false)
self.view_counts.create(:viewable => self, :ip_address => ip_address, :logged_in => logged_in)
return self
end
def view_count
self.view_counts.length
end
def view_count_string(str = "view")
return "#{view_count} #{str.singularize}" if view_count == 1
return "#{view_count} #{str.pluralize}" unless view_count == 1
end
end
end
end
ActiveRecord::Base.send(:include, Citrus::HasViewCount)

296
vendor/plugins/rcon/bin/rcontool vendored Normal file
View file

@ -0,0 +1,296 @@
#!/usr/bin/env ruby
################################################################
#
# rcontool - shell interface to rcon commands
#
# (C) 2006 Erik Hollensbe, License details below
#
# Use 'rcontool -h' for usage instructions.
#
# The compilation of software known as rcontool is distributed under the
# following terms:
# Copyright (C) 2005-2006 Erik Hollensbe. All rights reserved.
#
# Redistribution and use in source form, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
################################################################
#
# rubygems hack
#
begin
require 'rubygems'
rescue LoadError => e
end
begin
require 'rcon'
require 'ip'
rescue LoadError => e
$stderr.puts "rcontool requires the rcon and ip libraries be installed."
$stderr.puts "You can find them both via rubygems or at http://rubyforge.org."
exit -1
end
RCONTOOL_VERSION = '0.1.0'
require 'optparse'
require 'ostruct'
#
# Manages our options
#
def get_options
options = OpenStruct.new
# ip address (IP::Address object)
options.ip_address = nil
# port (integer)
options.port = nil
# password
options.password = nil
# protocol type (one of :hlds, :source, :oldquake, :newquake)
options.protocol_type = nil
# verbose, spit out extra information
options.verbose = false
# command to execute on the server
options.command = nil
optparse = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename $0} <ip_address:port> <command> [options]"
opts.separator ""
opts.separator "Options:"
opts.on("--ip-address [ADDRESS]",
"Provide an IP address to connect to. Does not take a port.") do |ip_address|
if ! options.ip_address.nil?
$stderr.puts "Error: you have already provided an IP Address."
$stderr.puts opts
exit -1
end
options.ip_address = IP::Address.new(ip_address)
end
opts.on("-r", "--port [PORT]",
"Port to connect to.") do |port|
if ! options.port.nil?
$stderr.puts "Error: you have already provided a port."
$stderr.puts opts
exit -1
end
options.port = port.to_i
end
opts.on("-c", "--command [COMMAND]",
"Command to run on the server.") do |command|
if ! options.command.nil?
$stderr.puts "Error: you have already provided a command."
$stderr.puts opts
exit -1
end
options.command = command
end
opts.on("-p", "--password [PASSWORD]",
"Provide a password on the command line.") do |password|
options.password = password
end
opts.on("-f", "--password-from [FILENAME]",
"Get the password from a file (use '/dev/fd/0' or '/dev/stdin' to read from Standard Input).") do |filename|
if !filename.nil?
f = File.open(filename)
options.password = f.gets.chomp
f.close
else
$stderr.puts "Error: filename (from -f) is not valid."
$stderr.puts opts
exit -1
end
end
opts.on("-t", "--protocol-type [TYPE]", [:hlds, :source, :oldquake, :newquake],
"Type of rcon connection to make: (hlds, source, oldquake, newquake).",
" Note: oldquake is quake1/quakeworld, newquake is quake2/3.") do |protocol_type|
options.protocol_type = protocol_type
end
opts.on("-v", "--[no-]verbose",
"Run verbosely, print information about each packet recieved and turnaround times.") do |verbose|
options.verbose = verbose
end
opts.on("-h", "--help",
"This help message.") do
$stderr.puts opts
exit -1
end
opts.on("--version", "Print the version information.") do
$stderr.puts "This is rcontool version #{RCONTOOL_VERSION},"
$stderr.puts "it is located at #{File.expand_path $0}."
exit -1
end
opts.separator ""
opts.separator "Note: IP, port, protocol type, password and command are required to function."
opts.separator ""
opts.separator "Examples (all are equivalent):"
opts.separator "\t#{File.basename($0)} 10.0.0.11 status -t hlds -r 27015 -p foobar"
opts.separator "\techo 'foobar' | #{File.basename($0)} 10.0.0.11:27015 status -t hlds -f /dev/stdin"
opts.separator "\t#{File.basename($0)} --ip-address 10.0.0.11 --port 27015 -c status -t hlds -f file_with_password"
opts.separator ""
end
################################################################
#
# This hackery is to help facilitate the bareword options if
# they exist, while still allowing for the option parser
# to work properly.
#
################################################################
s1 = ARGV.shift
s2 = ARGV.shift
begin
options.ip_address = IP::Address::IPv4.new(s1)
options.command = s2
rescue IP::AddressException => e
# attempt to split it first... not sure how to best handle this situation
begin
ip,port = s1.split(/:/, 2)
options.ip_address = IP::Address::IPv4.new(ip)
options.port = port.to_i
options.command = s2
rescue Exception => e
end
if [options.ip_address, options.port].include? nil
ARGV.unshift(s2)
ARGV.unshift(s1)
end
end
optparse.parse!
if [options.ip_address, options.protocol_type, options.port, options.password, options.command].include? nil
$stderr.puts optparse
exit -1
end
return options
end
def verbose(string)
$stderr.puts string if $options.verbose
end
def dump_source_packet(packet)
if $options.verbose
verbose "Request ID: #{packet.request_id}"
verbose "Packet Size: #{packet.packet_size}"
verbose "Response Type: #{packet.command_type}"
end
end
################################################################
#
# start main block
#
################################################################
$options = get_options
################################################################
#
# Source query
#
################################################################
if $options.protocol_type == :source
verbose "Protocol type 'SOURCE' selected."
rcon = RCon::Query::Source.new($options.ip_address.ip_address, $options.port)
# if we have a verbose request, give all the information we can about
# the query, including the packet information.
rcon.return_packets = $options.verbose
verbose "Attempting authentication to #{$options.ip_address.ip_address}:#{$options.port} with password '#{$options.password}'"
value = rcon.auth $options.password
dump_source_packet value
if ($options.verbose && value.command_type == RCon::Packet::Source::RESPONSE_AUTH) || value
verbose "Authentication succeeded. Sending command: '#{$options.command}'"
value = rcon.command $options.command
dump_source_packet value
verbose ""
if $options.verbose
puts value.string1
else
puts value
end
exit 0
else
$stderr.puts "Authentication failed."
exit 1
end
################################################################
#
# Original Query
#
################################################################
else
rcon = nil
case $options.protocol_type
when :hlds
verbose "Protocol type 'HLDS' selected"
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
RCon::Query::Original::HLDS)
when :oldquake
verbose "Protocol type 'OLDQUAKE' selected"
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
RCon::Query::Original::QUAKEWORLD)
when :newquake
verbose "Protocol type 'NEWQUAKE' selected"
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
RCon::Query::Original::NEWQUAKE)
end
verbose "Attempting transmission to #{$options.ip_address.ip_address}:#{$options.port}"
verbose "Using password: '#{$options.password}' and sending command: '#{$options.command}'"
verbose ""
string = rcon.command($options.command)
puts string
exit 0
end

499
vendor/plugins/rcon/lib/rcon.rb vendored Normal file
View file

@ -0,0 +1,499 @@
# encoding: US-ASCII
require 'socket'
#
# RCon is a module to work with Quake 1/2/3, Half-Life, and Half-Life
# 2 (Source Engine) RCon (Remote Console) protocols.
#
# Version:: 0.2.0
# Author:: Erik Hollensbe <erik@hollensbe.org>
# License:: BSD
# Contact:: erik@hollensbe.org
# Copyright:: Copyright (c) 2005-2006 Erik Hollensbe
#
# The relevant modules to query RCon are in the RCon::Query namespace,
# under RCon::Query::Original (for Quake 1/2/3 and Half-Life), and
# RCon::Query::Source (for HL2 and CS: Source, and other Source Engine
# games). The RCon::Packet namespace is used to manage complex packet
# structures if required. The Original protocol does not require
# this, but Source does.
#
# Usage is fairly simple:
#
# # Note: Other classes have different constructors
#
# rcon = RCon::Query::Source.new("10.0.0.1", 27015)
#
# rcon.auth("foobar") # source only
#
# rcon.command("mp_friendlyfire") => "mp_friendlyfire = 1"
#
# rcon.cvar("mp_friendlyfire") => 1
#
#--
#
# The compilation of software known as rcon.rb is distributed under the
# following terms:
# Copyright (C) 2005-2006 Erik Hollensbe. All rights reserved.
#
# Redistribution and use in source form, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#++
class RCon
class Packet
# placeholder so ruby doesn't bitch
end
class Query
#
# Convenience method to scrape input from cvar output and return that data.
# Returns integers as a numeric type if possible.
#
# ex: rcon.cvar("mp_friendlyfire") => 1
#
def cvar(cvar_name)
response = command(cvar_name)
match = /^.+?\s(?:is|=)\s"([^"]+)".*$/.match response
match = match[1]
if /\D/.match match
return match
else
return match.to_i
end
end
end
end
#
# RCon::Packet::Source generates a packet structure useful for
# RCon::Query::Source protocol queries.
#
# This class is primarily used internally, but is available if you
# want to do something more advanced with the Source RCon
# protocol.
#
# Use at your own risk.
#
class RCon::Packet::Source
# execution command
COMMAND_EXEC = 2
# auth command
COMMAND_AUTH = 3
# auth response
RESPONSE_AUTH = 2
# normal response
RESPONSE_NORM = 0
# packet trailer
TRAILER = "\x00\x00"
# size of the packet (10 bytes for header + string1 length)
attr_accessor :packet_size
# Request Identifier, used in managing multiple requests at once
attr_accessor :request_id
# Type of command, normally COMMAND_AUTH or COMMAND_EXEC. In response packets, RESPONSE_AUTH or RESPONSE_NORM
attr_accessor :command_type
# First string, the only used one in the protocol, contains
# commands and responses. Null terminated.
attr_accessor :string1
# Second string, unused by the protocol. Null terminated.
attr_accessor :string2
#
# Generate a command packet to be sent to an already
# authenticated RCon connection. Takes the command as an
# argument.
#
def command(string)
@request_id = rand(1000)
@string1 = string
@string2 = TRAILER
@command_type = COMMAND_EXEC
@packet_size = build_packet.length
return self
end
#
# Generate an authentication packet to be sent to a newly
# started RCon connection. Takes the RCon password as an
# argument.
#
def auth(string)
@request_id = rand(1000)
@string1 = string
@string2 = TRAILER
@command_type = COMMAND_AUTH
@packet_size = build_packet.length
return self
end
#
# Builds a packet ready to deliver, without the size prepended.
# Used to calculate the packet size, use #to_s to get the packet
# that srcds actually needs.
#
def build_packet
return [@request_id, @command_type, @string1, @string2].pack("VVa#{@string1.length}a2")
end
# Returns a string representation of the packet, useful for
# sending and debugging. This include the packet size.
def to_s
packet = build_packet
@packet_size = packet.length
return [@packet_size].pack("V") + packet
end
end
#
# RCon::Query::Original queries Quake 1/2/3 and Half-Life servers
# with the rcon protocol. This protocol travels over UDP to the
# game server port, and requires an initial authentication step,
# the information of which is provided at construction time.
#
# Some of the work here (namely the RCon packet structure) was taken
# from the KKRcon code, which is written in perl.
#
# One query per authentication is allowed.
#
class RCon::Query::Original < RCon::Query
# HLDS-Based Servers
HLDS = "l"
# QuakeWorld/Quake 1 Servers
QUAKEWORLD = "n"
# Quake 2/3 Servers
NEWQUAKE = ""
# Request to be sent to server
attr_reader :request
# Response from server
attr_reader :response
# Challenge ID (served by server-side of connection)
attr_reader :challenge_id
# UDPSocket object
attr_reader :socket
# Host of connection
attr_reader :host
# Port of connection
attr_reader :port
# RCon password
attr_reader :password
# type of server
attr_reader :server_type
#
# Creates a RCon::Query::Original object for use.
#
# The type (the default of which is HLDS), has multiple possible
# values:
#
# HLDS - Half Life 1 (will not work with older versions of HLDS)
#
# QUAKEWORLD - QuakeWorld/Quake 1
#
# NEWQUAKE - Quake 2/3 (and many derivatives)
#
def initialize(host, port, password, type=HLDS)
@host = host
@port = port
@password = password
@server_type = type
end
#
# Sends a request given as the argument, and returns the
# response as a string.
#
def command(request)
@request = request
@challenge_id = nil
establish_connection
@socket.print "\xFF" * 4 + "challenge rcon\n\x00"
tmp = retrieve_socket_data
challenge_id = /challenge rcon (\d+)/.match tmp
if challenge_id
@challenge_id = challenge_id[1]
end
if @challenge_id.nil?
raise RCon::NetworkException.new("RCon challenge ID never returned: wrong rcon password?")
end
@socket.print "\xFF" * 4 + "rcon #{@challenge_id} \"#{@password}\" #{@request}\n\x00"
@response = retrieve_socket_data
@response.sub!(/^\xFF\xFF\xFF\xFF#{@server_type}/, "")
@response.sub!(/\x00+$/, "")
return @response
end
#
# Disconnects the RCon connection.
#
def disconnect
if @socket
@socket.close
@socket = nil
end
end
protected
#
# Establishes the connection.
#
def establish_connection
if @socket.nil?
@socket = UDPSocket.new
@socket.connect(@host, @port)
end
end
#
# Generic method to pull data from the socket.
#
def retrieve_socket_data
return "" if @socket.nil?
retval = ""
loop do
break unless IO.select([@socket], nil, nil, 10)
packet = @socket.recv(8192)
retval << packet
break if packet.length < 8192
end
return retval
end
end
#
# RCon::Query::Source sends queries to a "Source" Engine server,
# such as Half-Life 2: Deathmatch, Counter-Strike: Source, or Day
# of Defeat: Source.
#
# Note that one authentication packet needs to be sent to send
# multiple commands. Sending multiple authentication packets may
# damage the current connection and require it to be reset.
#
# Note: If the attribute 'return_packets' is set to true, the full
# RCon::Packet::Source object is returned, instead of just a string
# with the headers stripped. Useful for debugging.
#
class RCon::Query::Source < RCon::Query
# RCon::Packet::Source object that was sent as a result of the last query
attr_reader :packet
# TCPSocket object
attr_reader :socket
# Host of connection
attr_reader :host
# Port of connection
attr_reader :port
# Authentication Status
attr_reader :authed
# return full packet, or just data?
attr_accessor :return_packets
#
# Given a host and a port (dotted-quad or hostname OK), creates
# a RCon::Query::Source object. Note that this will still
# require an authentication packet (see the auth() method)
# before commands can be sent.
#
def initialize(host, port)
@host = host
@port = port
@socket = nil
@packet = nil
@authed = false
@return_packets = false
end
#
# See RCon::Query#cvar.
#
def cvar(cvar_name)
return_packets = @return_packets
@return_packets = false
response = super
@return_packets = return_packets
return response
end
#
# Sends a RCon command to the server. May be used multiple times
# after an authentication is successful.
#
# See the class-level documentation on the 'return_packet' attribute
# for return values. The default is to return a string containing
# the response.
#
def command(command)
if ! @authed
raise RCon::NetworkException.new("You must authenticate the connection successfully before sending commands.")
end
@packet = RCon::Packet::Source.new
@packet.command(command)
@socket.print @packet.to_s
rpacket = build_response_packet
if rpacket.command_type != RCon::Packet::Source::RESPONSE_NORM
raise RCon::NetworkException.new("error sending command: #{rpacket.command_type}")
end
if @return_packets
return rpacket
else
return rpacket.string1
end
end
#
# Requests authentication from the RCon server, given a
# password. Is only expected to be used once.
#
# See the class-level documentation on the 'return_packet' attribute
# for return values. The default is to return a true value if auth
# succeeded.
#
def auth(password)
establish_connection
@packet = RCon::Packet::Source.new
@packet.auth(password)
@socket.print @packet.to_s
# on auth, one junk packet is sent
rpacket = nil
2.times { rpacket = build_response_packet }
if rpacket.command_type != RCon::Packet::Source::RESPONSE_AUTH
raise RCon::NetworkException.new("error authenticating: #{rpacket.command_type}")
end
@authed = true
if @return_packets
return rpacket
else
return true
end
end
alias_method :authenticate, :auth
#
# Disconnects from the Source server.
#
def disconnect
if @socket
@socket.close
@socket = nil
@authed = false
end
end
protected
#
# Builds a RCon::Packet::Source packet based on the response
# given by the server.
#
def build_response_packet
rpacket = RCon::Packet::Source.new
total_size = 0
request_id = 0
type = 0
response = ""
message = ""
loop do
break unless IO.select([@socket], nil, nil, 10)
#
# TODO: clean this up - read everything and then unpack.
#
tmp = @socket.recv(14)
if tmp.nil?
return nil
end
size, request_id, type, message = tmp.unpack("VVVa*")
total_size += size
# special case for authentication
break if message.sub!(/\x00\x00$/, "")
response << message
# the 'size - 10' here accounts for the fact that we've snarfed 14 bytes,
# the size (which is 4 bytes) is not counted, yet represents the rest
# of the packet (which we have already taken 10 bytes from)
tmp = @socket.recv(size - 10)
response << tmp
response.sub!(/\x00\x00$/, "")
end
rpacket.packet_size = total_size
rpacket.request_id = request_id
rpacket.command_type = type
# strip nulls (this is actually the end of string1 and string2)
rpacket.string1 = response.sub(/\x00\x00$/, "")
return rpacket
end
# establishes a connection to the server.
def establish_connection
if @socket.nil?
@socket = TCPSocket.new(@host, @port)
end
end
end
# Exception class for network errors
class RCon::NetworkException < Exception
end

13
vendor/plugins/rcon/rcon.gemspec vendored Normal file
View file

@ -0,0 +1,13 @@
spec = Gem::Specification.new
spec.name = "rcon"
spec.version = "0.2.1"
spec.author = "Erik Hollensbe"
spec.email = "erik@hollensbe.org"
spec.summary = "Ruby class to work with Quake 1/2/3, Half-Life and Source Engine rcon (remote console)"
spec.has_rdoc = true
spec.autorequire = "rcon"
spec.bindir = 'bin'
spec.executables << 'rcontool'
spec.add_dependency('ip', '>= 0.2.1')
spec.files = Dir['lib/rcon.rb'] + Dir['bin/rcontool']
spec.rubyforge_project = 'rcon'

1360
vendor/plugins/rcon/setup.rb vendored Normal file

File diff suppressed because it is too large Load diff