has_content 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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