Skip to content

Commit 93e13b0

Browse files
committed
Merge branch 'specs'
2 parents e86ab56 + 8226062 commit 93e13b0

23 files changed

Lines changed: 639 additions & 238 deletions

.rspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--color --format documentation

README.md

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,37 @@ Usage
3030

3131
Sprinkle a little magic into an existing model:
3232

33-
class User < ActiveRecord::Base
33+
class Person < ActiveRecord::Base
3434
has_magic_columns
3535
end
3636

3737
Add magic columns to your model:
3838

39-
@bob = User.create(:email => "bob@example.com")
40-
@bob.magic_columns.create(:name => "first_name")
39+
@charlie = Person.create(:email => "charlie@example.com")
40+
@charlie.magic_columns.create(:name => "first_name")
4141

4242
Supply additional options if you have more specific requirements for your columns:
4343

44-
@bob.magic_columns.create(:name => "last_name", :is_required => true)
45-
@bob.magic_columns.create(:name => "birthday", :datatype => :date)
46-
@bob.magic_columns.create(:name => "salary", :default => "40000", :pretty_name => "Yearly Salary")
44+
@charlie.magic_columns.create(:name => "last_name", :is_required => true)
45+
@charlie.magic_columns.create(:name => "birthday", :datatype => :date)
46+
@charlie.magic_columns.create(:name => "salary", :default => "40000", :pretty_name => "Yearly Salary")
4747

4848
The :datatype option supports :check_box_boolean, :date, :datetime, or :integer.
4949

5050
Use your new columns just like you would with any other ActiveRecord attribute:
5151

52-
@bob.first_name = "Bob"
53-
@bob.last_name = "Magic!"
54-
@bob.birthday = Date.today
55-
@bob.save
52+
@charlie.first_name = "Charlie"
53+
@charlie.last_name = "Magic!"
54+
@charlie.birthday = Date.today
55+
@charlie.save
5656

57-
Find @bob and inspect him:
57+
Find @charlie and inspect him:
5858

59-
@bob = User.find(@bob.id)
60-
@bob.first_name #=> "Bob"
61-
@bob.last_name #=> "Magic!"
62-
@bob.birthday #=> #<Date: 4908497/2,0,2299161>
63-
@bob.salary #=> "40000", this is from :salary having a :default
59+
@charlie = User.find(@charlie.id)
60+
@charlie.first_name #=> "Charlie"
61+
@charlie.last_name #=> "Magic!"
62+
@charlie.birthday #=> #<Date: 4908497/2,0,2299161>
63+
@charlie.salary #=> "40000", this is from :salary having a :default
6464

6565
## Inherited Model
6666

@@ -71,43 +71,42 @@ as having magic columns:
7171
has_many :users
7272
has_magic_columns
7373
end
74+
@account = Account.create(:name => "BobCorp")
7475

7576
And declare the child as having magic columns :through the parent.
7677

7778
class User < ActiveRecord::Base
7879
belongs_to :account
7980
has_magic_columns :through => :account
8081
end
82+
@alice = User.create(:name => "alice", :account => @account)
8183

8284
To see all the magic columns available for a child from its parent:
8385

84-
@user.magic_columns #=> [#<MagicColumn>,...]
85-
@user.account.magic_columns #=> [#<MagicColumn>,...]
86+
@alice.magic_columns #=> [#<MagicColumn>,...]
87+
@account.magic_columns #=> [#<MagicColumn>,...]
88+
@alice.account.magic_columns #=> [#<MagicColumn>,...]
8689

8790
To add magic columns, go through the parent or child:
8891

89-
@user.magic_columns.create(...)
90-
@user.account.magic_columns.create(...)
92+
@alice.magic_columns.create(...)
93+
@account.magic_columns.create(...)
9194

9295
All children for a given parent will have access to the same magic columns:
9396

94-
@account = Account.create(:name => "BobCorp")
97+
@alice.magic_columns.create(:name => "salary")
98+
@alice.salary = "40000"
9599

96100
@bob = User.create(:name => "bob", :account => @account)
97-
@bob.magic_columns.create(:name => "salary")
98-
@bob.salary = "40000"
99-
100-
@steve = User.create(:name => "bob", :account => @account)
101-
# no need to add the column again
102-
@steve.salary = "50000"
101+
# Magic! No need to add the column again!
102+
@bob.salary = "50000"
103103

104104
To Do
105105
=====
106106

107-
This gem is mostly functional. Here's a short list of things that need to be
108-
done to polish it up:
107+
Here's a short list of things that need to be done to polish up this gem:
109108

110-
* Test
109+
* Test other parts of the data model (e.g. magic_attributes, magic_options)
111110
* Benchmark and optimize
112111

113112
Maintainers
@@ -119,3 +118,9 @@ Maintainers
119118
Contribute
120119
==========
121120
See the [CONTRIBUTORS guide](https://github.com/latortuga/has_magic_columns/blob/master/CONTRIBUTORS.md).
121+
122+
Credits
123+
=======
124+
125+
* Thank you to Brandon Keene for his original work making this plugin.
126+
* Thank you to the [will_paginate](https://github.com/mislav/will_paginate) gem for iinspiration and code examples for how to test a Rails plugin.

Rakefile

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
require 'rake'
22
gem 'rdoc'
33
require 'rdoc/task'
4-
require 'rake/testtask'
4+
require 'rspec/core/rake_task'
55

66
require 'bundler'
77
Bundler::GemHelper.install_tasks
88

9-
#desc 'Default: run unit tests.'
10-
task :default => :test
11-
12-
Rake::TestTask.new do |t|
13-
t.libs << 'lib'
14-
t.pattern = 'test/**/*_test.rb'
15-
t.verbose = true
16-
end
17-
189
desc 'Generate documentation.'
1910
RDoc::Task.new do |rdoc|
2011
rdoc.main = "README.md"
@@ -23,3 +14,18 @@ RDoc::Task.new do |rdoc|
2314
rdoc.title = 'HasMagicColumns'
2415
rdoc.options << '--line-numbers' << '--inline-source'
2516
end
17+
18+
task :default => :spec
19+
20+
desc 'Run specs'
21+
RSpec::Core::RakeTask.new(:spec) do |t|
22+
t.pattern = 'spec/**/*_spec.rb'
23+
t.ruby_opts = "-Ilib:spec"
24+
end
25+
26+
namespace :spec do
27+
desc "Run Rails specs"
28+
RSpec::Core::RakeTask.new(:rails) do |t|
29+
t.pattern = %w'spec/finders/active_record_spec.rb spec/view_helpers/action_view_spec.rb'
30+
end
31+
end

has_magic_columns.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
1414
s.date = Date.today
1515

1616
s.files = `git ls-files`.split("\n")
17-
s.test_files = `git ls-files -- {test,spec,features}/`.split("\n")
17+
s.test_files = `git ls-files -- spec/`.split("\n")
1818
s.require_paths = ["lib"]
1919

2020
s.add_dependency("activesupport", ["~> 3.0"])

init.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
ActiveRecord::Base.send :include, Has::Magic::Columns
1+
require 'has_magic_columns'
2+
3+
if defined? ActiveRecord::Base
4+
require 'has_magic_columns/active_record'
5+
end

lib/has_magic_columns.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
require 'has_magic_columns/has_magic_columns'
2-
require 'has_magic_columns/version'
3-
require 'has_magic_columns/railtie' if defined?(Rails)
4-
51
# Has Magic Columns
62
#
73
# Copyright (c) 2007 Brandon Keene <bkeene AT gmail DOT com>
@@ -22,3 +18,11 @@
2218
module HasMagicColumns # :nodoc:
2319
end
2420

21+
require 'has_magic_columns/version'
22+
23+
if defined? Rails::Railtie
24+
require 'has_magic_columns/railtie'
25+
elsif defined? Rails::Initializer
26+
$stderr.puts "\nhas_magic_columns is not compatible with Rails 2, use at your own risk.\n\n"
27+
end
28+
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
require 'active_support'
2+
require 'active_record'
3+
4+
module HasMagicColumns #:nodoc:
5+
module ActiveRecord
6+
module ClassMethods
7+
def has_magic_columns(options = {})
8+
unless magical?
9+
# Associations
10+
has_many :magic_attribute_relationships, :as => :owner, :dependent => :destroy
11+
has_many :magic_attributes, :through => :magic_attribute_relationships, :dependent => :destroy
12+
13+
# Eager loading - EXPERIMENTAL!
14+
if options[:eager]
15+
class_eval do
16+
def after_initialize
17+
initialize_magic_columns
18+
end
19+
end
20+
end
21+
22+
# Inheritence
23+
cattr_accessor :inherited_from
24+
25+
# if options[:through] is supplied, treat as an inherited relationship
26+
if self.inherited_from = options[:through]
27+
class_eval do
28+
def inherited_magic_columns
29+
raise "Cannot inherit MagicColumns from a non-existant association: #{@inherited_from}" unless self.class.method_defined?(inherited_from)# and self.send(inherited_from)
30+
self.send(inherited_from).magic_columns
31+
end
32+
end
33+
alias_method :magic_columns, :inherited_magic_columns unless method_defined? :magic_columns
34+
35+
# otherwise the calling model has the relationships
36+
else
37+
has_many :magic_column_relationships, :as => :owner, :dependent => :destroy
38+
has_many :magic_columns, :through => :magic_column_relationships, :dependent => :destroy
39+
end
40+
41+
# Hook into Base
42+
class_eval do
43+
alias_method :reload_without_magic, :reload
44+
alias_method :create_or_update_without_magic, :create_or_update
45+
alias_method :read_attribute_without_magic, :read_attribute
46+
end
47+
end
48+
include InstanceMethods
49+
50+
# Add Magic to Base
51+
alias_method :reload, :reload_with_magic
52+
alias_method :read_attribute, :read_attribute_with_magic
53+
alias_method :create_or_update, :create_or_update_with_magic
54+
end
55+
56+
def magical?
57+
self.included_modules.include?(InstanceMethods)
58+
end
59+
end
60+
61+
module InstanceMethods #:nodoc:
62+
# Reinitialize MagicColumns and MagicAttributes when Model is reloaded
63+
def reload_with_magic
64+
initialize_magic_columns
65+
reload_without_magic
66+
end
67+
68+
def update_attributes(new_attributes)
69+
attributes = new_attributes.stringify_keys
70+
magic_attrs = magic_columns.map(&:name)
71+
72+
super(attributes.select{ |k, v| !magic_attrs.include?(k) })
73+
attributes.select{ |k, v| magic_attrs.include?(k) }.each do |k, v|
74+
col = find_magic_column_by_name(k)
75+
attr = find_magic_attribute_by_column(col).first
76+
attr.update_attributes(:value => v)
77+
end
78+
end
79+
80+
private
81+
82+
# Save MagicAttributes from @attributes
83+
def create_or_update_with_magic
84+
if result = create_or_update_without_magic
85+
magic_columns.each do |column|
86+
value = @attributes[column.name]
87+
existing = find_magic_attribute_by_column(column)
88+
89+
unless column.datatype == 'check_box_multiple'
90+
(attr = existing.first) ?
91+
update_magic_attribute(attr, value) :
92+
create_magic_attribute(column, value)
93+
else
94+
#TODO - make this more efficient
95+
value = [value] unless value.is_a? Array
96+
existing.map(&:destroy) if existing
97+
value.collect {|v| create_magic_attribute(column, v)}
98+
end
99+
end
100+
end
101+
result
102+
end
103+
104+
# Load (lazily) MagicAttributes or fall back
105+
def method_missing(method_id, *args)
106+
super(method_id, *args)
107+
rescue NoMethodError
108+
method_name = method_id.to_s
109+
attr_names = magic_columns.map(&:name)
110+
initialize_magic_columns and retry if attr_names.include?(method_name) or
111+
(md = /[\?|\=]/.match(method_name) and
112+
attr_names.include?(md.pre_match))
113+
super(method_id, *args)
114+
end
115+
116+
# Load the MagicAttribute(s) associated with attr_name and cast them to proper type.
117+
def read_attribute_with_magic(attr_name)
118+
return read_attribute_without_magic(attr_name) if column_for_attribute(attr_name) # filter for regular columns
119+
attr_name = attr_name.to_s
120+
121+
if !(value = @attributes[attr_name]).nil?
122+
if column = find_magic_column_by_name(attr_name)
123+
if value.is_a? Array
124+
value.map {|v| column.type_cast(v)}
125+
else
126+
column.type_cast(value)
127+
end
128+
else
129+
value
130+
end
131+
else
132+
nil
133+
end
134+
end
135+
136+
# Lookup all MagicAttributes and setup @attributes
137+
def initialize_magic_columns
138+
magic_columns.each do |column|
139+
attribute = find_magic_attribute_by_column(column)
140+
name = column.name
141+
142+
# Validation
143+
self.class.validates_presence_of(name) if column.is_required?
144+
145+
# Write attribute
146+
unless column.datatype == 'check_box_multiple'
147+
(attr = attribute.first) ?
148+
write_attribute(name, attr.to_s) :
149+
write_attribute(name, column.default)
150+
else
151+
write_attribute(name, attribute.map(&:to_s))
152+
end
153+
end
154+
end
155+
156+
def find_magic_attribute_by_column(column)
157+
magic_attributes.to_a.find_all {|attr| attr.magic_column_id == column.id}
158+
end
159+
160+
def find_magic_column_by_name(attr_name)
161+
magic_columns.to_a.find {|column| column.name == attr_name}
162+
end
163+
164+
def create_magic_attribute(magic_column, value)
165+
magic_attributes << MagicAttribute.create(:magic_column => magic_column, :value => value)
166+
end
167+
168+
def update_magic_attribute(magic_attribute, value)
169+
magic_attribute.update_attributes(:value => value)
170+
end
171+
end
172+
173+
# mix into Active Record
174+
::ActiveRecord::Base.extend ClassMethods
175+
176+
%w{ models }.each do |dir|
177+
path = File.join(File.dirname(__FILE__), '../app', dir)
178+
$LOAD_PATH << path
179+
ActiveSupport::Dependencies.autoload_paths << path
180+
ActiveSupport::Dependencies.autoload_once_paths.delete(path)
181+
end
182+
end
183+
end

0 commit comments

Comments
 (0)