two_faced 0.0.2.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,41 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .idea/.name
20
+
21
+ .idea/.rakeTasks
22
+
23
+ .idea/dictionaries/digital466.xml
24
+
25
+ .idea/two_faced.iml
26
+
27
+ .idea/vcs.xml
28
+
29
+ .idea/workspace.xml
30
+
31
+ .idea/scopes/scope_settings.xml
32
+
33
+ .idea/modules.xml
34
+
35
+ .idea/misc.xml
36
+
37
+ .idea/encodings.xml
38
+
39
+ .gitignore
40
+
41
+ .gitignore
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in two_faced.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Kevin Horst
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # TwoFaced
2
+
3
+ You've developed a site...and a mobile site...and facebook tab. Everything pulls in the same data and is working swimmingly, until you get a call from your boss/client --
4
+
5
+ "Hi, can we use a different photo for the Facebook version of this article? Also, the title of the Parmesan Crusted Chicken Cutlets is too long for mobile, can we just call them "Cheesy Cutlets" for mobile? "
6
+
7
+ At this point you have a few options:
8
+
9
+ 1. Say no. They only get one image and one title and have to deal with it. Congratulations! You've just saved yourself a hassle.
10
+
11
+ 2. Add new columns like "facebook_image" and "mobile_title" for these edge cases
12
+
13
+ 3. Duplicate the records for each context and scope your queries accordingly. (Recipe.where(:context => "facebook").all ) This works, but the duplication can be a pain
14
+
15
+ 4. Use TwoFaced to monkey-patch your data! Only override the attributes that are different.
16
+
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'two_faced', :git => "git://github.com/krhorst/two_faced.git"
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+
29
+ After you install Two Faced and add it to your Gemfile, you need to run the generator:
30
+
31
+ $ rails generate two_faced:install
32
+ $ rake db:migrate
33
+
34
+ ## Configuring Models
35
+
36
+ Add the following line into any model you want to be overrideable:
37
+
38
+ acts_as_overrideable
39
+
40
+ ## Adding overrides with Active Admin
41
+
42
+ Add the nested attributes to the form for the resource, like so:
43
+
44
+ form do |f|
45
+ f.inputs
46
+ f.has_many :overrides do |o|
47
+ o.input :context_name
48
+ o.input :field_name, :as => :select, :collection => f.object.attribute_names
49
+ o.input :field_value
50
+ end
51
+ f.buttons
52
+ end
53
+
54
+ ## Adding overrides using the nested_form gem
55
+
56
+ Add the nested fields for overrides.
57
+
58
+ <%= nested_form_for(@yourrecord) do |f| %>
59
+
60
+ ## Your existing Fields
61
+
62
+ <%= f.fields_for :overrides do |o| %>
63
+ <div class="field">
64
+ <%= f.label :context_name %><br />
65
+ <%= o.text_field :context_name %><br/>
66
+ </div>
67
+ <div class="field">
68
+ <%= f.label :field_name %><br />
69
+ <%= o.select :field_name, f.object.attribute_names.map{ |value| [value, value]} %>
70
+ </div>
71
+ <div class="field">
72
+ <%= f.label :field_value %><br />
73
+ <%= o.text_field :field_value %>
74
+ </div>
75
+ <% end %>
76
+
77
+ <%= f.link_to_add 'Add Override', :overrides %>
78
+
79
+ <div class="actions">
80
+ <%= f.submit %>
81
+ </div>
82
+ <% end %>
83
+
84
+ ## Adding overrides programatically
85
+
86
+ @yourrecord.overrides.create(:field_name => "name", :field_value => "New Name", :context_name => "mobile")
87
+
88
+ ## Usage
89
+
90
+ You can now load up a version of your model with all overrides merged in. By default, two_faced will not overwrite the property, but will create a new method that will return the overridden property or the original property (if no override exists).
91
+
92
+ @example = ModelName.for_context("facebook").first
93
+ @example.name # Will be the original name
94
+ @example.overridden_name # Will be the overridden name
95
+
96
+ If the overwrite parameter is passed, both the original property and the overridden property will be set to the new value
97
+
98
+ @example = ModelName.for_context("facebook", :overwrite => true).first
99
+ @example.name # Will be the overridden name
100
+ @example.overridden_name # Will be the overridden name
101
+
102
+ If overwrite is set to true, it will also mark any records as read-only (to prevent saving this context-specific value back to the database). So this will give you an error:
103
+
104
+ @example = ModelName.for_context("facebook", :overwrite => true).first
105
+ @example.save # ActiveRecord::ReadOnlyRecord exception
106
+
107
+ You can also pass a custom prefix which will be used in place of "overridden". An underscore will be appended to this, and then the property name
108
+
109
+ @example = ModelName.for_context("facebook", :overwrite => true, :attribute_prefix = "custom").first
110
+ @example.name # Will be the overridden name
111
+ @example.custom_name # Will be the overridden name
112
+
113
+
114
+ ## Contributing
115
+
116
+ 1. Fork it
117
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
118
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
119
+ 4. Push to the branch (`git push origin my-new-feature`)
120
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ task default: :spec
@@ -0,0 +1,17 @@
1
+ class Override < ActiveRecord::Base
2
+ attr_accessible :context_name, :field_name, :field_value, :overrideable_id, :overrideable_type
3
+
4
+ belongs_to :overrideable, :polymorphic => true
5
+
6
+ validates :field_name, :presence => true
7
+ validates :context_name, :presence => true
8
+ validate :field_name_exists_on_associated_model
9
+
10
+ def field_name_exists_on_associated_model
11
+ klass = overrideable_type.constantize
12
+ unless klass.attribute_names.include?(field_name)
13
+ errors.add(:field_name, "must be an attribute of #{overrideable_type}")
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,24 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module TwoFaced
4
+ module Generators
5
+ class InstallGenerator < ActiveRecord::Generators::Base
6
+ desc "Installs TwoFaced and generates the necessary migrations"
7
+
8
+ argument :name, :type => :string, :default => "create_migrations"
9
+
10
+ def self.source_root
11
+ File.expand_path("../templates", __FILE__)
12
+ end
13
+
14
+
15
+ def create_migrations
16
+ Dir["#{self.class.source_root}/migrations/*.rb"].sort.each do |filepath|
17
+ name = File.basename(filepath)
18
+ migration_template "migrations/#{name}", "db/migrate/#{name.gsub(/^\d+_/,'')}"
19
+ sleep 1
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class CreateOverrides < ActiveRecord::Migration
2
+ def change
3
+ create_table :overrides do |t|
4
+ t.string :context_name
5
+ t.string :overrideable_type
6
+ t.integer :overrideable_id
7
+ t.string :field_name
8
+ t.text :field_value
9
+
10
+ t.timestamps
11
+ end
12
+ add_index :overrides, :context_name
13
+ add_index :overrides, :overrideable_type
14
+ add_index :overrides, :overrideable_id
15
+ add_index :overrides, :field_name
16
+ end
17
+ end
data/lib/two_faced.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "two_faced/version"
2
+ require "two_faced/hook"
3
+ require "two_faced/model_extensions"
4
+ require "two_faced/railtie"
5
+ require "two_faced/engine"
@@ -0,0 +1,10 @@
1
+ require 'two_faced'
2
+ require 'rails'
3
+
4
+ module TwoFaced
5
+ class Engine < Rails::Engine
6
+
7
+ config.mount_at = '/'
8
+
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module TwoFaced::Hook
2
+ def acts_as_overrideable
3
+ include TwoFaced::ModelExtensions
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ module TwoFaced
2
+
3
+ module ModelExtensions
4
+
5
+ def self.included(base)
6
+ base.attr_accessible :overrides_attributes
7
+ base.has_many :overrides, :as => :overrideable, :dependent => :destroy
8
+ base.accepts_nested_attributes_for :overrides
9
+ base.after_find :get_overrides_for_context
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ @overwrite_attribute_prefix = "overridden"
16
+
17
+ def context
18
+ @context
19
+ end
20
+
21
+ def overwrite_attribute_prefix
22
+ @overwrite_attribute_prefix
23
+ end
24
+
25
+ def overwrite_attributes?
26
+ @overwrite_attributes
27
+ end
28
+
29
+ def for_context(context, options = {})
30
+ options = {
31
+ :attribute_prefix => "overridden",
32
+ :overwrite => false
33
+ }.merge(options)
34
+
35
+ @context = context
36
+ includes(:overrides)
37
+ @overwrite_attributes = options[:overwrite]
38
+ @overwrite_attribute_prefix = options[:attribute_prefix]
39
+ self
40
+ end
41
+
42
+
43
+ end
44
+
45
+ def get_overrides_for_context
46
+ klass = self.class
47
+ self.overrides.where("context_name" => klass.context).each do |override|
48
+ singleton_class.class_eval { attr_accessor "#{klass.overwrite_attribute_prefix}_#{override.field_name}" }
49
+ send "#{klass.overwrite_attribute_prefix}_#{override.field_name}=", override.field_value
50
+ if self.class.overwrite_attributes?
51
+ send "#{override.field_name}=", override.field_value
52
+ send "readonly!"
53
+ end
54
+ end
55
+ self
56
+ end
57
+
58
+ def method_missing(method_name, *args)
59
+ prefix = "#{self.class.overwrite_attribute_prefix}_"
60
+ if method_name.to_s[prefix] != nil
61
+ original_method = method_name.to_s.sub(prefix, "")
62
+ send original_method
63
+ else
64
+ super
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,8 @@
1
+ module TwoFaced
2
+ class Railtie < Rails::Railtie
3
+ if defined?(ActiveRecord::Base)
4
+ ActiveRecord::Base.send :extend, TwoFaced::Hook
5
+ end
6
+ end
7
+
8
+ end
@@ -0,0 +1,3 @@
1
+ module TwoFaced
2
+ VERSION = "0.0.2.alpha"
3
+ end
@@ -0,0 +1,34 @@
1
+ require "spec_helper"
2
+
3
+ describe Override do
4
+
5
+ before(:each) do
6
+ @example_model = ModelToOverride.create({:name => "Name", :description => "Description"})
7
+ @valid_override_attributes = {
8
+ :context_name => "website",
9
+ :field_name => "name",
10
+ :field_value => "New Name" }
11
+ end
12
+
13
+ it "should create an override given valid attributes" do
14
+ @example_model.overrides.new(@valid_override_attributes)
15
+ @example_model.should be_valid
16
+ end
17
+
18
+ it "should not create an override without a field name" do
19
+ no_field_attributes = @valid_override_attributes.merge(:field_name => "")
20
+ @example_model.overrides.new(no_field_attributes).should_not be_valid
21
+ end
22
+
23
+ it "should not create an override without a context name" do
24
+ no_context_attributes = @valid_override_attributes.merge(:context_name => "")
25
+ @example_model.overrides.new(no_context_attributes).should_not be_valid
26
+ end
27
+
28
+ it "should not create an override if the field name does not exist on the model" do
29
+ no_field_attributes = @valid_override_attributes.merge(:field_name => "nonexistent_field")
30
+ @example_model.overrides.new(no_field_attributes).should_not be_valid
31
+ end
32
+
33
+
34
+ end
@@ -0,0 +1,45 @@
1
+ require "rails"
2
+ require "active_record"
3
+ require "two_faced"
4
+
5
+ ActiveRecord::Migration.verbose = false
6
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
7
+
8
+ def setup_db
9
+ ActiveRecord::Schema.define(:version => 0) do
10
+
11
+ create_table :overrides do |t|
12
+ t.string :context_name
13
+ t.string :overrideable_type
14
+ t.integer :overrideable_id
15
+ t.string :field_name
16
+ t.text :field_value
17
+
18
+ t.timestamps
19
+ end
20
+
21
+ create_table :model_to_overrides do |t|
22
+ t.string :name
23
+ t.string :description
24
+
25
+ t.timestamps
26
+ end
27
+ end
28
+ end
29
+
30
+
31
+ setup_db
32
+
33
+ def cleanup_db
34
+ ActiveRecord::Base.connection.tables.each do |table|
35
+ ActiveRecord::Base.connection.execute("delete from #{table}")
36
+ end
37
+ end
38
+
39
+ class ModelToOverride < ActiveRecord::Base
40
+ attr_accessible :name, :description
41
+ acts_as_overrideable
42
+ end
43
+
44
+
45
+ load(File.expand_path( 'app/models/override.rb'))
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ describe TwoFaced do
4
+
5
+ after(:each) do
6
+ cleanup_db
7
+ end
8
+
9
+ before(:each) do
10
+ @record = ModelToOverride.create(:name => "Original Name", :description => "Original Description")
11
+ end
12
+
13
+ it "should show the original name normally" do
14
+ @record.name.should eq("Original Name")
15
+ end
16
+
17
+ describe "with mobile override added" do
18
+
19
+ before(:each) do
20
+ @record.overrides.create(:field_name => "name", :field_value => "New Name", :context_name => "mobile")
21
+ end
22
+
23
+ it "should still show the original name if not scoped" do
24
+ @record.name.should eq("Original Name")
25
+ end
26
+
27
+ it "should have a different overridden_name if scoped for mobile" do
28
+ mobile_version = ModelToOverride.for_context("mobile").first
29
+ mobile_version.overridden_name.should eq("New Name")
30
+ mobile_version.name.should eq("Original Name")
31
+ end
32
+
33
+ it "should overwrite the name property if passing the overwrite parameter" do
34
+ overwritten_mobile_version = ModelToOverride.for_context("mobile", :overwrite => true).first
35
+ overwritten_mobile_version.overridden_name.should eq("New Name")
36
+ overwritten_mobile_version.name.should eq("New Name")
37
+ end
38
+
39
+ it "should give read only error if attempting to save an overridden record" do
40
+ overwritten_mobile_version = ModelToOverride.for_context("mobile", :overwrite => true).first
41
+ expect { overwritten_mobile_version.save }.to raise_error
42
+ end
43
+
44
+ it "should show original name if override does not match scope" do
45
+ facebook_version = ModelToOverride.for_context("facebook", :overwrite => true).first
46
+ facebook_version.name.should eq("Original Name")
47
+ facebook_version.overridden_name.should eq("Original Name")
48
+ end
49
+
50
+ it "should create a property with a custom prefix" do
51
+ custom_prefixed_version = ModelToOverride.for_context("mobile", :attribute_prefix => "custom").first
52
+ custom_prefixed_version.name.should eq("Original Name")
53
+ custom_prefixed_version.custom_name.should eq("New Name")
54
+ end
55
+
56
+ end
57
+
58
+ end
data/two_faced.gemspec ADDED
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/two_faced/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kevin Horst"]
6
+ gem.email = ["krhorst@gmail.com"]
7
+ gem.description = "Monkey-patch your data"
8
+ gem.summary = "Add context-specific overrides to model attributes without new columns"
9
+ gem.homepage = "http://github.com/krhorst/two_faced"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "two_faced"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = TwoFaced::VERSION
17
+ gem.add_development_dependency "rspec"
18
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: two_faced
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Kevin Horst
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Monkey-patch your data
31
+ email:
32
+ - krhorst@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - app/models/override.rb
43
+ - lib/generators/two_faced/install/install_generator.rb
44
+ - lib/generators/two_faced/install/templates/migrations/1_create_overrides.rb
45
+ - lib/two_faced.rb
46
+ - lib/two_faced/engine.rb
47
+ - lib/two_faced/hook.rb
48
+ - lib/two_faced/model_extensions.rb
49
+ - lib/two_faced/railtie.rb
50
+ - lib/two_faced/version.rb
51
+ - spec/override_spec.rb
52
+ - spec/spec_helper.rb
53
+ - spec/two_faced_spec.rb
54
+ - two_faced.gemspec
55
+ homepage: http://github.com/krhorst/two_faced
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>'
71
+ - !ruby/object:Gem::Version
72
+ version: 1.3.1
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.24
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Add context-specific overrides to model attributes without new columns
79
+ test_files:
80
+ - spec/override_spec.rb
81
+ - spec/spec_helper.rb
82
+ - spec/two_faced_spec.rb