has_content 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ = 0.0.1
2
+ * Initial Release
data/MIT-LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright 2012 Ian White
2
+
3
+ This plugin was developed by Ian White (http://github.com/ianwhite)
4
+ while working at Distinctive Doors (http://distinctivedoors.co.uk/oss)
5
+ who have kindly agreed to release this under the MIT-LICENSE.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ has_content [![Build Status](https://secure.travis-ci.org/i2w/has_content.png?branch=master)](http://travis-ci.org/i2w/has_content)
2
+ ======
3
+
4
+ Add a content relation to any active_record, which acts like a text field, but is actually a separate object.
5
+
6
+ This means it's very easy to add/remove fields, but also the contents can be objects in their own right (useful for editing CMS stuff etc)
7
+
8
+ This was developed by [Ian White](http://github.com/ianwhite) while working at [Distinctive Doors](http://distinctivedoors.co.uk/oss) who have kindly agreed to release this under the MIT-LICENSE.
9
+
10
+ License
11
+ -------
12
+
13
+ This project uses the MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'HasContent'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.md', 'CHANGELOG', 'MIT-LICENSE')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ require 'rspec/core/rake_task'
24
+
25
+ RSpec::Core::RakeTask.new(:spec)
26
+
27
+ desc "Run the specs with simplecov"
28
+ task :simplecov => [:simplecov_env, :spec]
29
+ task :simplecov_env do ENV['SIMPLECOV'] = '1' end
30
+
31
+ task :default => :spec
32
+
33
+ Bundler::GemHelper.install_tasks
34
+
35
+ task :release => :check_gemfile
36
+
37
+ task :check_gemfile do
38
+ if File.exists?("Gemfile.lock") && File.read("Gemfile.lock") != File.read("Gemfile.lock.development")
39
+ cp "Gemfile.lock", "Gemfile.lock.development"
40
+ raise "** Gemfile.lock.development has been updated, please commit these changes."
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ class CreateHasContentRecords < ActiveRecord::Migration
2
+ def change
3
+ create_table :has_content_records, :force => true do |t|
4
+ t.belongs_to :owner, :polymorphic => true
5
+ t.string :name
6
+ t.text :content
7
+ t.timestamps
8
+ end
9
+ add_index :has_content_records, [:owner_id, :owner_type, :name], :unique => true
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+ module HasContent
2
+ # has_content concern: allow spcification of content on models
3
+ #
4
+ # content is a polymorphic association that is laoded on demand, and appears as if it were a normal attribute.
5
+ # The advantages being that you can add content to models without modifying the database schema, and that you
6
+ # can treat pieces of content as objects in their own right (for example in place editing).
7
+ #
8
+ # class MyModel
9
+ # has_content :body, :sidebar
10
+ # end
11
+ module ActiveRecord
12
+ extend ActiveSupport::Concern
13
+
14
+ module ClassMethods
15
+ # specify that this class has the following named content
16
+ def has_content *names
17
+ include ContentOwner unless self < ContentOwner
18
+ options = names.extract_options!
19
+ raise ArgumentError, "you must supply at least one content name" if names.size == 0
20
+ names.each {|name| add_content(name.to_s, options)}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ module HasContent
2
+ module ContentOwner
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ delegate :content_names, :content_association_names, :to => 'self.class'
7
+ class_attribute :content_names
8
+ self.content_names ||= []
9
+ end
10
+
11
+ # return a hash of content attributes with their content only (for loaded content only)
12
+ def content_attributes
13
+ content_names.inject({}) do |attrs, name|
14
+ attrs.merge!(name => send(name)) if send("#{name}_content")
15
+ attrs
16
+ end
17
+ end
18
+
19
+ # include content_attributes in attributes hash
20
+ def attributes
21
+ super.merge content_attributes
22
+ end
23
+
24
+ module ClassMethods
25
+ def content_association_names
26
+ content_names.map {|name| "#{name}_content"}
27
+ end
28
+
29
+ protected
30
+ def add_content name, options = {}
31
+ raise ArgumentError, "name should be suitable for a simple attribute method" unless name =~ /\A[_a-z][_a-z0-9]+\Z/i
32
+ if content_names.include?(name)
33
+ raise ArgumentError, "Content #{name} is already declared in #{self.name}"
34
+ else
35
+ add_content_association name, options
36
+ end
37
+ end
38
+
39
+ def add_content_association name, options
40
+ content_names << name
41
+
42
+ has_one "#{name}_content".to_sym, options.reverse_merge(:as => 'owner', :class_name => 'HasContent::Record', :dependent => :destroy, :conditions => {name: name}, :autosave => true)
43
+
44
+ define_method name do
45
+ send("find_or_build_#{name}_content").content
46
+ end
47
+
48
+ define_method "#{name}=" do |value|
49
+ (send("find_or_build_#{name}_content").content = value).tap do |*|
50
+ if respond_to?(:updated_at?) && send("find_or_build_#{name}_content").changed?
51
+ updated_at_will_change!
52
+ end
53
+ end
54
+ end
55
+
56
+ define_method "find_or_build_#{name}_content" do
57
+ send("#{name}_content") || send("build_#{name}_content", name: name)
58
+ end
59
+ private "find_or_build_#{name}_content".to_sym
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ module HasContent
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,42 @@
1
+ module HasContent
2
+ class Record < ActiveRecord::Base
3
+ self.table_name = 'has_content_records'
4
+
5
+ belongs_to :owner, polymorphic: true
6
+
7
+ validates :name, presence: true,
8
+ inclusion: {in: lambda(&:allowed_names), if: :owner},
9
+ uniqueness: {scope: %w(owner_id owner_type), if: :owner_persisted?}
10
+
11
+ before_create :verify_valid_owner! # this badboy is here because owner is sometimes not present at validation
12
+ # because content is a has_one on owner, with autosave true
13
+
14
+ # Contents are only ever instantiated by has_content assoc, and it's convenient for them to
15
+ # always refer (for links to content etc).
16
+ # If there is a validation problem, the save will fail silently (which is fine)
17
+ def initialize(*)
18
+ super
19
+ save if new_record? && owner_persisted?
20
+ end
21
+
22
+ def to_s
23
+ content
24
+ end
25
+
26
+ def allowed_names
27
+ owner.try(:content_names) || []
28
+ end
29
+
30
+ protected
31
+
32
+ def owner_persisted?
33
+ owner && !owner.new_record?
34
+ end
35
+
36
+ # see the before_create hook above
37
+ def verify_valid_owner!
38
+ owner.reload # raises ActiveRecord::RecordNotFound if owner not found
39
+ valid? or raise ActiveRecord::RecordInvalid, self
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module HasContent
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'has_content/version'
2
+ require 'has_content/record'
3
+ require 'has_content/content_owner'
4
+ require 'has_content/active_record'
5
+ require 'has_content/engine' if defined?(Rails)
6
+
7
+ ActiveSupport.on_load(:active_record) do
8
+ include HasContent::ActiveRecord
9
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'owner_shared'
3
+
4
+ describe ContentOwner do
5
+ let(:owner) { described_class.new }
6
+ let(:content_name) { :body }
7
+
8
+ it_should_behave_like "new owner with has_content"
9
+
10
+ its(:content_names) { should == ['body', 'excerpt', 'sidebar'] }
11
+
12
+ its(:content_association_names) { should == ['body_content', 'excerpt_content', 'sidebar_content'] }
13
+
14
+ describe "subclass" do
15
+ let(:klass) { Class.new(described_class) }
16
+ let(:owner) { klass.new }
17
+
18
+ it_should_behave_like "new owner with has_content"
19
+
20
+ context 'after adding new content :thingo' do
21
+ before(:all) do klass.has_content :thingo end
22
+
23
+ its(:content_names) { should == ['body', 'excerpt', 'sidebar', 'thingo'] }
24
+
25
+ its(:content_association_names) { should == ['body_content', 'excerpt_content', 'sidebar_content', 'thingo_content'] }
26
+ end
27
+
28
+ context 'when a content attribute is protected' do
29
+ before(:all) do klass.attr_protected :excerpt end
30
+
31
+ it 'setting via attributes fails' do
32
+ owner.attributes = {excerpt: 'foo', body: 'bar'}
33
+ owner.excerpt.should be_nil
34
+ owner.body.should == 'bar'
35
+ owner.save
36
+ owner.reload.body_content.content.should == 'bar'
37
+ owner.reload.excerpt_content.content.should be_nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe HasContent::ActiveRecord do
4
+ subject { content_owner }
5
+ let(:content_owner) { Class.new(ActiveRecord::Base) }
6
+
7
+ describe '.has_content(name)' do
8
+ subject { content_owner.has_content(name) }
9
+
10
+ describe 'requires name be in a format suitable for a simple accessor method' do
11
+ ['foo bar', '123bar', 'foo!', 'foo=', 'foo-bar', 'foo+bar'].each do |invalid|
12
+ context invalid.inspect do
13
+ let(:name) { invalid }
14
+ it { expect { subject }.to raise_error ArgumentError }
15
+ end
16
+ end
17
+
18
+ ['_foo', 'foo_bar', 'a123bar', 'foo', 'FooBar'].each do |valid|
19
+ context valid.inspect do
20
+ let(:name) { valid }
21
+ it { expect { subject }.to_not raise_error }
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ it '.has_content() raises ArgumentError' do
28
+ expect{ content_owner.has_content }.to raise_error(ArgumentError)
29
+ end
30
+
31
+ it '.has_content(<extsiting name>) raises ArgumentError' do
32
+ content_owner.has_content(:foo)
33
+ expect{ content_owner.has_content(:foo) }.to raise_error(ArgumentError)
34
+ end
35
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe HasContent::Record do
4
+ subject { record }
5
+
6
+ let(:record) { described_class.new attrs }
7
+ let(:attrs) { {} }
8
+
9
+ it "does not save itself - as it's invalid" do
10
+ should be_new_record
11
+ end
12
+
13
+ describe 'with valid attributes' do
14
+ let(:attrs) { {:name => 'body', :owner => owner} }
15
+ let(:owner) { ContentOwner.create! }
16
+
17
+ it "saves itself (to enable always referring relationships)" do
18
+ should_not be_new_record
19
+ end
20
+
21
+ it { should be_valid }
22
+
23
+ describe '[validation]' do
24
+ it 'requires :name' do
25
+ subject.name = nil
26
+ should_not be_valid
27
+ end
28
+
29
+ it 'requires :name + :owner be unique' do
30
+ record.save!
31
+ record = described_class.new :name => 'body', :owner => owner
32
+ record.should_not be_valid
33
+ record.name = 'excerpt'
34
+ record.should be_valid
35
+ record.name = 'body'
36
+ record.owner = ContentOwner.create!
37
+ record.should be_valid
38
+ end
39
+
40
+ it 'requires :name be one of owner\'s content_names' do
41
+ record.name = 'not_there'
42
+ record.should_not be_valid
43
+ end
44
+ end
45
+ end
46
+
47
+ describe '#to_s' do
48
+ subject { record.to_s }
49
+
50
+ it 'is the content attribute' do
51
+ record.content = 'foo'
52
+ subject.should == 'foo'
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ shared_examples_for "new owner with has_content" do
2
+
3
+ # set the following to use this shared spec:
4
+ #
5
+ # let(:owner) { a content owner instance }
6
+ # let(:content_name) { the name of the content }
7
+
8
+ it 'should be built on demand for #content reader' do
9
+ owner.send(content_name)
10
+ owner.send("#{content_name}_content").should be_kind_of(HasContent::Record)
11
+ end
12
+
13
+ it 'should be built on demand for #content= writer' do
14
+ owner.send("#{content_name}=", 'foo')
15
+ owner.send("#{content_name}_content").content.should == 'foo'
16
+ end
17
+
18
+ it "content should have name == content_name" do
19
+ owner.send(content_name)
20
+ owner.send("#{content_name}_content").name.should == content_name.to_s
21
+ end
22
+
23
+ it "after save and reload, should have content" do
24
+ owner.send("#{content_name}=", "foo")
25
+ owner.save!
26
+ owner.class.find(owner.id).send(content_name).should == "foo"
27
+ end
28
+
29
+ it 'should have content_attributes corresponding to contents' do
30
+ owner.send("#{content_name}=", "Foo")
31
+ owner.content_attributes[content_name.to_s].should == "Foo"
32
+ end
33
+
34
+ it "should allow setting content via attributes" do
35
+ owner.update_attributes content_name => "Foo"
36
+ owner.reload.send(content_name).should == "Foo"
37
+ end
38
+
39
+ context '[before being saved]' do
40
+ it "should not create a content record until save" do
41
+ lambda { owner.send(content_name) }.should_not change(HasContent::Record, :count)
42
+ lambda { owner.save! }.should change(HasContent::Record, :count).by(1)
43
+ end
44
+ end
45
+
46
+ context '[after being saved]' do
47
+ before { owner.save! }
48
+
49
+ it 'should create content record on access' do
50
+ lambda { owner.send(content_name) }.should change(HasContent::Record, :count).by(1)
51
+ lambda { owner.save! }.should_not change(HasContent::Record, :count)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+
3
+ if ENV['SIMPLECOV']
4
+ require 'simplecov'
5
+ SimpleCov.start do
6
+ add_filter "_spec.rb"
7
+ end
8
+ end
9
+
10
+ begin
11
+ require 'pry'
12
+ rescue LoadError
13
+ end
14
+
15
+ require 'active_record'
16
+ require 'rspec'
17
+ require 'database_cleaner'
18
+
19
+ require_relative '../lib/has_content'
20
+ require_relative 'test_app'
21
+
22
+ DatabaseCleaner.strategy = :truncation
23
+ DatabaseCleaner.clean_with :truncation
24
+
25
+ RSpec.configure do |config|
26
+ config.treat_symbols_as_metadata_keys_with_true_values = true
27
+
28
+ config.before(:each) { DatabaseCleaner.start }
29
+ config.after(:each) { DatabaseCleaner.clean }
30
+ end
data/spec/test_app.rb ADDED
@@ -0,0 +1,18 @@
1
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
2
+
3
+ ActiveRecord::Migration.suppress_messages do
4
+ ActiveRecord::Schema.define do
5
+ create_table(:content_owners, :force => true) do |t|
6
+ t.string :name
7
+ t.timestamps
8
+ end
9
+
10
+ require_relative '../db/migrate/create_has_content_records'
11
+ CreateHasContentRecords.new.change
12
+ end
13
+ end
14
+
15
+ class ContentOwner < ActiveRecord::Base
16
+ has_content :body
17
+ has_content :excerpt, :sidebar
18
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_content
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ian White
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70244228437940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70244228437940
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70244228459060 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70244228459060
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &70244228456400 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '2'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70244228456400
47
+ - !ruby/object:Gem::Dependency
48
+ name: sqlite3
49
+ requirement: &70244228453880 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70244228453880
58
+ - !ruby/object:Gem::Dependency
59
+ name: database_cleaner
60
+ requirement: &70244228452440 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70244228452440
69
+ description: Simple wrapper for adding content via a polymorphic join for any active
70
+ record. Version 0.0.2.
71
+ email:
72
+ - ian.w.white@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - db/migrate/create_has_content_records.rb
78
+ - lib/has_content/active_record.rb
79
+ - lib/has_content/content_owner.rb
80
+ - lib/has_content/engine.rb
81
+ - lib/has_content/record.rb
82
+ - lib/has_content/version.rb
83
+ - lib/has_content.rb
84
+ - MIT-LICENSE
85
+ - Rakefile
86
+ - README.md
87
+ - CHANGELOG
88
+ - spec/content_owner_spec.rb
89
+ - spec/has_content/active_record_spec.rb
90
+ - spec/has_content/record_spec.rb
91
+ - spec/owner_shared.rb
92
+ - spec/spec_helper.rb
93
+ - spec/test_app.rb
94
+ homepage: http://github.com/i2w/has_content
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ segments:
107
+ - 0
108
+ hash: -2447319576099344314
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ segments:
116
+ - 0
117
+ hash: -2447319576099344314
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 1.8.15
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Simple wrapper for adding content via a polymorphic join for any active record
124
+ test_files:
125
+ - spec/content_owner_spec.rb
126
+ - spec/has_content/active_record_spec.rb
127
+ - spec/has_content/record_spec.rb
128
+ - spec/owner_shared.rb
129
+ - spec/spec_helper.rb
130
+ - spec/test_app.rb