Skip to content

Commit

Permalink
Got full coverage of what we need, just model based stuff for now
Browse files Browse the repository at this point in the history
  • Loading branch information
ianwhite committed Jun 20, 2012
1 parent ba4e587 commit c95a3c9
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 65 deletions.
2 changes: 2 additions & 0 deletions .rspec
@@ -0,0 +1,2 @@
--colour
--format progress
3 changes: 2 additions & 1 deletion Gemfile
Expand Up @@ -5,7 +5,8 @@ source "http://rubygems.org"
# development dependencies will be added by default to the :development group.
gemspec

group :test do
group :development do
gem 'simplecov', :require => false
gem 'pry'
gem 'guard-rspec'
end
6 changes: 6 additions & 0 deletions Guardfile
@@ -0,0 +1,6 @@
guard 'rspec', :version => 2 do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch(%r{spec/.*(shared|helper).rb}) { "spec" }
end

5 changes: 3 additions & 2 deletions lib/has_content/active_record.rb
Expand Up @@ -2,7 +2,8 @@ module HasContent
# has_content concern: allow spcification of content on models
#
# content is a polymorphic association that is laoded on demand, and appears as if it were a normal attribute.
# The advantage being that you can add content to models without modifying the database schema.
# The advantages being that you can add content to models without modifying the database schema, and that you
# can treat pieces of content as objects in their own right (for example in place editing).
#
# class MyModel
# has_content :body, :sidebar
Expand All @@ -15,7 +16,7 @@ module ClassMethods
def has_content *names
include ContentOwner unless self < ContentOwner
options = names.extract_options!
names = ['body'] if names.size == 0
raise ArgumentError, "you must supply at least one content name" if names.size == 0
names.each {|name| add_content(name.to_s, options)}
end
end
Expand Down
40 changes: 18 additions & 22 deletions lib/has_content/content_owner.rb
Expand Up @@ -3,19 +3,16 @@ module ContentOwner
extend ActiveSupport::Concern

included do
delegate :content_names, :content_associations, :to => 'self.class'
delegate :content_names, :content_association_names, :to => 'self.class'
class_attribute :content_names
self.content_names = []
self.content_names ||= []
end

# return a hash of content attributes with their content only (for loaded content only)
def content_attributes
content_names.inject({}) do |attrs, name|
if send("#{name}_content")
attrs.merge(name => send(name))
else
attrs
end
attrs.merge!(name => send(name)) if send("#{name}_content")
attrs
end
end

Expand All @@ -25,11 +22,7 @@ def attributes
end

module ClassMethods
def content_names
read_inheritable_attribute(:content_names) || write_inheritable_attribute(:content_names, [])
end

def content_associations
def content_association_names
content_names.map {|name| "#{name}_content"}
end

Expand All @@ -46,21 +39,24 @@ def add_content name, options = {}
def add_content_association name, options
content_names << name

has_one "#{name}_content", options.reverse_merge(:as => 'owner', :class_name => 'HasContent::Record', :dependent => :destroy, :conditions => {name: name}, :autosave => true)

define_method "autobuilt_#{name}_content" do
send("#{name}_content") || send("build_#{name}_content", name: name)
end

define_method name do
send("autobuilt_#{name}_content").content
has_one "#{name}_content".to_sym, options.reverse_merge(:as => 'owner', :class_name => 'HasContent::Record', :dependent => :destroy, :conditions => {name: name}, :autosave => true)

define_method name do
send("find_or_build_#{name}_content").content
end

define_method "#{name}=" do |value|
tap(send("autobuilt_#{name}_content").content = value) do |_|
updated_at_will_change! if send("#{name}_content").changed?
(send("find_or_build_#{name}_content").content = value).tap do |*|
if respond_to?(:updated_at?) && send("find_or_build_#{name}_content").changed?
updated_at_will_change!
end
end
end

define_method "find_or_build_#{name}_content" do
send("#{name}_content") || send("build_#{name}_content", name: name)
end
private "find_or_build_#{name}_content".to_sym
end
end
end
Expand Down
25 changes: 17 additions & 8 deletions lib/has_content/record.rb
Expand Up @@ -4,17 +4,19 @@ class Record < ActiveRecord::Base

belongs_to :owner, polymorphic: true

validates :owner, presence: true
validates :name, presence: true, inclusion: {in: lambda(&:allowed_names)}, uniqueness: {scope: %w(owner_id owner_type)}
validates :name, presence: true,
inclusion: {in: lambda(&:allowed_names), if: :owner},
uniqueness: {scope: %w(owner_id owner_type), if: :owner_persisted?}

before_create :check_existence_of_owner # this badboy is here because owner is sometimes not present at validation
# because content is a has_one on owner, with autosave true
before_create :verify_valid_owner! # this badboy is here because owner is sometimes not present at validation
# because content is a has_one on owner, with autosave true

# Contents are only ever instantiated by has_content assoc, and it's convenient for them to
# always refer. If they aren't then the save below will fail silently (which is fine)
# always refer (for links to content etc).
# If there is a validation problem, the save will fail silently (which is fine)
def initialize(*)
super
save if new_record? && owner
save if new_record? && owner_persisted?
end

def to_s
Expand All @@ -25,9 +27,16 @@ def allowed_names
owner.try(:content_names) || []
end

protected
def check_existence_of_owner
protected

def owner_persisted?
owner && !owner.new_record?
end

# see the before_create hook above
def verify_valid_owner!
owner.reload # raises ActiveRecord::RecordNotFound if owner not found
valid? or raise ActiveRecord::RecordInvalid, self
end
end
end
37 changes: 36 additions & 1 deletion spec/content_owner_spec.rb
@@ -1,6 +1,41 @@
require 'spec_helper'
require 'owner_shared'

describe ContentOwner do
it 'should be foo' do
let(:owner) { described_class.new }
let(:content_name) { :body }

it_should_behave_like "new owner with has_content"

its(:content_names) { should == ['body', 'excerpt', 'sidebar'] }

its(:content_association_names) { should == ['body_content', 'excerpt_content', 'sidebar_content'] }

describe "subclass" do
let(:klass) { Class.new(described_class) }
let(:owner) { klass.new }

it_should_behave_like "new owner with has_content"

context 'after adding new content :thingo' do
before(:all) do klass.has_content :thingo end

its(:content_names) { should == ['body', 'excerpt', 'sidebar', 'thingo'] }

its(:content_association_names) { should == ['body_content', 'excerpt_content', 'sidebar_content', 'thingo_content'] }
end

context 'when a content attribute is protected' do
before(:all) do klass.attr_protected :excerpt end

it 'setting via attributes fails' do
owner.attributes = {excerpt: 'foo', body: 'bar'}
owner.excerpt.should be_nil
owner.body.should == 'bar'
owner.save
owner.reload.body_content.content.should == 'bar'
owner.reload.excerpt_content.content.should be_nil
end
end
end
end
9 changes: 9 additions & 0 deletions spec/has_content/active_record_spec.rb
Expand Up @@ -23,4 +23,13 @@
end
end
end

it '.has_content() raises ArgumentError' do
expect{ content_owner.has_content }.to raise_error(ArgumentError)
end

it '.has_content(<extsiting name>) raises ArgumentError' do
content_owner.has_content(:foo)
expect{ content_owner.has_content(:foo) }.to raise_error(ArgumentError)
end
end
14 changes: 9 additions & 5 deletions spec/has_content/record_spec.rb
Expand Up @@ -21,11 +21,6 @@
it { should be_valid }

describe '[validation]' do
it 'requires :owner' do
subject.owner = nil
should_not be_valid
end

it 'requires :name' do
subject.name = nil
should_not be_valid
Expand All @@ -48,4 +43,13 @@
end
end
end

describe '#to_s' do
subject { record.to_s }

it 'is the content attribute' do
record.content = 'foo'
subject.should == 'foo'
end
end
end
53 changes: 27 additions & 26 deletions spec/owner_shared.rb
@@ -1,19 +1,23 @@
# let(:owner) { a content owner instance }
# let(:content_name) { the name of the content }
describe "HasContent::ContentOwner", :shared => true do
shared_examples_for "new owner with has_content" do

# set the following to use this shared spec:
#
# let(:owner) { a content owner instance }
# let(:content_name) { the name of the content }

it 'should be built on demand for #content reader' do
owner.send(content_name)
owner.instance_eval("@#{content_name}_content").should be_kind_of(HasHasContent::Content::HasContent::Content)
owner.send("#{content_name}_content").should be_kind_of(HasContent::Record)
end

it 'should be built on demand for #content= writer' do
owner.send("#{content_name}=", 'foo')
owner.instance_eval("@#{content_name}_content").content.should == 'foo'
owner.send("#{content_name}_content").content.should == 'foo'
end

it "content should have name == content_name" do
owner.send(content_name)
owner.send("#{content_name}_content").name.should == content_name
owner.send("#{content_name}_content").name.should == content_name.to_s
end

it "after save and reload, should have content" do
Expand All @@ -22,32 +26,29 @@
owner.class.find(owner.id).send(content_name).should == "foo"
end

it "should not load assoc on parent save, if assoc not already loaded" do
owner.should_not_receive("#{content_name}_content")
owner.save!
end

it "when owner is a new_record, should not create a content until save" do
if owner.new_record?
lambda { owner.send(content_name) }.should_not change(HasContent::Content, :count)
lambda { owner.save }.should change(HasContent::Content, :count).by(1)
end
end

it 'when owner is not new_record, should not create a content until save'
if !owner.new_record?
lambda { owner.send(content_name) }.should_not change(HasContent::Content, :count)
lambda { owner.save }.should change(HasContent::Content, :count).by(1)
end
end

it 'should have content_attributes corresponding to contents' do
owner.send("#{content_name}=", "Foo")
owner.content_attributes[content_name].should == "Foo"
owner.content_attributes[content_name.to_s].should == "Foo"
end

it "should allow setting content via attributes" do
owner.update_attributes content_name => "Foo"
owner.reload.send(content_name).should == "Foo"
end

context '[before being saved]' do
it "should not create a content record until save" do
lambda { owner.send(content_name) }.should_not change(HasContent::Record, :count)
lambda { owner.save! }.should change(HasContent::Record, :count).by(1)
end
end

context '[after being saved]' do
before { owner.save! }

it 'should create content record on access' do
lambda { owner.send(content_name) }.should change(HasContent::Record, :count).by(1)
lambda { owner.save! }.should_not change(HasContent::Record, :count)
end
end
end
1 change: 1 addition & 0 deletions spec/test_app.rb
Expand Up @@ -4,6 +4,7 @@
ActiveRecord::Schema.define do
create_table(:content_owners, :force => true) do |t|
t.string :name
t.timestamps
end

require_relative '../db/migrate/create_has_content_records'
Expand Down

0 comments on commit c95a3c9

Please sign in to comment.