one_touch 1.0.0

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/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ rails/*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ .rbenv-version
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in one_touch.gemspec
4
+ gemspec
5
+
6
+ # gem 'cancan', '~> 1.6' # when add controller code in gem
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'rspec-rails'
10
+ gem 'ruby-debug19'
11
+ gem 'rails', '3.0.10'
12
+ gem 'meta_where', '~> 1.0.0'
13
+ # gem 'squeel' # add another version to support it and Rails3.1
14
+ gem 'sqlite3', '~> 1.3.0', :group => :test
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc "Running Test"
4
+ task :test do
5
+ system "bundle exec rspec spec/models/*.rb --debugger --backtrace --fail-fast"
6
+ end
@@ -0,0 +1,4 @@
1
+ module OneTouch
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,142 @@
1
+ require File.join(File.dirname(__FILE__),'acts_as_favor')
2
+ require File.join(File.dirname(__FILE__),'as_favor_host')
3
+ require File.join(File.dirname(__FILE__),'as_favorable')
4
+ require File.join(File.dirname(__FILE__),'relations')
5
+ require File.join(File.dirname(__FILE__),'host_relation')
6
+
7
+ module OneTouch
8
+ module ActiveRecordExtension
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ # Default is define two relations:
18
+ # belongs_to :host, :class_name => "User"
19
+ # belongs_to :favorable, :polymorphic => true
20
+ # Set acts_as_favor :host_class_name => nil to override default host relation by
21
+ # belongs_to :host, :polymorphic => true
22
+ # User can define his own specific relations, but make sure two relations name must be host and favorable
23
+ # and use acts_as_favor :no_default_relations => false after user defined relations
24
+ # Ex: belongs_to :host, :class_name => "AnyOne", :primary_key => :any_key
25
+ # belongs_to :tag
26
+ # acts_as_favor :no_default_relations => true
27
+ def acts_as_favor(opt={})
28
+
29
+ opt.reverse_merge!({:include_host_relation_module => true, :host_class_name => "User", :no_default_relations => false})
30
+ class_attribute :host_relation_type, :favorable_relation_type, :instance_writer => false
31
+
32
+ method_name_of_host_klasses = "host_klasses_for_#{name.underscore}"
33
+
34
+ include ActsAsFavor
35
+
36
+ unless opt[:no_default_relations]
37
+ if opt[:host_class_name].nil?
38
+ belongs_to :host, :polymorphic => true
39
+ host_relation_type = :polymorphic
40
+ if respond_to? method_name_of_host_klasses
41
+ send(method_name_of_host_klasses).each do |k|
42
+ k.to_s.constantize.rewrite_join_favors if k.to_s.constantize.respond_to? :rewrite_join_favors
43
+ end
44
+ end
45
+ else
46
+ belongs_to :host, :class_name => opt[:host_class_name]
47
+ host_relation_type = :single
48
+ end
49
+ if opt[:favorable_class_name].nil?
50
+ belongs_to :favorable, :polymorphic => true
51
+ favorable_relation_type = :polymorphic
52
+ else
53
+ belongs_to :favorable, :class_name => opt[:favorable_class_name]
54
+ favorable_relation_type = :single
55
+ end
56
+ end
57
+
58
+ if opt[:include_host_relation_module]
59
+ class_attribute :include_host_relation_module, :instance_writer => false
60
+ self.include_host_relation_module = true
61
+ add_host_relation_map(favorable_klasses, :be_favors, :host)
62
+ end
63
+
64
+ if respond_to? method_name_of_host_klasses
65
+ send(method_name_of_host_klasses).each do |k|
66
+ k.to_s.constantize.after_load_acts_as_favor if k.to_s.constantize.respond_to? :after_load_acts_as_favor
67
+ end
68
+ end
69
+ end
70
+
71
+ # Used in host class
72
+ # Ex:
73
+ # as_favor_host :klass => "OtherClass"
74
+ #
75
+ def as_favor_host(opt={})
76
+ opt = { :klass => "Favor", :no_default_relations => false }.merge opt
77
+
78
+ assign_cattr_in_favor_class(:host_klasses,opt)
79
+ # The Name of has_many is stick to favors, to make it changable would cause some query problem
80
+ # :as => :host can not be configured too, cause in act_as_favor, the relation name was sticked
81
+ unless opt[:no_default_relations]
82
+ has_many :favors, :class_name => opt[:klass], :as => :host
83
+ end
84
+ include AsFavorHost
85
+ end
86
+
87
+ # Used in favorable class
88
+ def as_favorable(opt={})
89
+ opt = { :klass => "Favor", :no_default_relations => false }.merge opt
90
+
91
+ assign_cattr_in_favor_class(:favorable_klasses,opt)
92
+ unless opt[:no_default_relations]
93
+ has_many :be_favors, :class_name => opt[:klass], :as => :favorable
94
+ end
95
+ include AsFavorable
96
+ include HostRelation if favor_class.respond_to? :include_host_relation_module and favor_class.include_host_relation_module = true
97
+ end
98
+
99
+ private
100
+
101
+ def add_host_relation_map(favorable_klasses, *two_connection_name)
102
+ class_attribute :favorable_to_media, :media_to_host, :instance_writer => false
103
+ unless respond_to? :favorable_klasses
104
+ class_attribute :favorable_klasses
105
+ self.favorable_klasses = favorable_klasses
106
+ end
107
+ self.favorable_to_media = two_connection_name.first
108
+ self.media_to_host = two_connection_name[1]
109
+
110
+ include Relations
111
+ self.target_to_media = :be_favors
112
+ self.media_to_host = :host
113
+
114
+ favorable_klasses.each do |k|
115
+ klass = k.to_s.constantize
116
+ klass.send :include, HostRelation # host_relation model is added to favorable klass
117
+ end
118
+ end
119
+
120
+ # Programmer Doc
121
+ # To maintain the connection betwwen host, favorable and favor class,
122
+ # I defined class_attribute in there super class AR::Base
123
+ # I don't know if there is any good solution to not disturb AR::Base
124
+ def assign_cattr_in_favor_class(cattr_name,opt)
125
+ class_attribute :favor_klass, :favor_class, :instance_writer => false
126
+ self.favor_klass = opt[:klass]
127
+ self.favor_class = favor_klass.constantize
128
+
129
+ # favor_class.acts_as_favor :just_define_host_klasses => true unless favor_class.respond_to? :host_klasses
130
+ # favor.class.acts_as_favor
131
+ cattr_name = "#{cattr_name}_for_#{opt[:klass].underscore}".to_sym
132
+ unless ::ActiveRecord::Base.respond_to? cattr_name
133
+ ::ActiveRecord::Base.class_attribute cattr_name, :instance_writer => false
134
+ ::ActiveRecord::Base.send "#{cattr_name}=", []
135
+ end
136
+ (::ActiveRecord::Base.send cattr_name) << self.name.to_sym
137
+ end
138
+
139
+ end # End ClassMethods
140
+
141
+ end # ActiveRecordExtension
142
+ end
@@ -0,0 +1,153 @@
1
+ # -*- coding: utf-8 -*-
2
+ module OneTouch
3
+ module ActsAsFavor
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ class_attribute :cons_context, :cons_context_match, :context_relation, :instance_writer => false
9
+ self.cons_context = self::CONTEXT
10
+ self.cons_context_match = self::CONTEXT_MATCH
11
+ self.context_relation = self::CONTEXT_RELATION if defined? CONTEXT_RELATION
12
+
13
+ # Seems there are many hook work to do, oppose hook, belongs hook ....
14
+ # All needed hook should be just about relations between contexts
15
+ # Other hooks should be write by user
16
+ after_save :context_relation_hook if context_relation.presence
17
+
18
+ validates_inclusion_of :context,:in => cons_context, :message => "%{value} not included in CONTEXT"
19
+
20
+ validate :context_should_match_favorable_type
21
+ validate :host_should_differ_favorable
22
+
23
+ scope :context_as, lambda { |context| where( { :context => context } ) if context.present? }
24
+
25
+ # Generate build or destroy methods like:
26
+ # Favor.add_focus, Favor.del_focus
27
+ cons_context.each do |context|
28
+ generate_del_methods context
29
+ generate_add_methods context
30
+ end
31
+
32
+ # generate scope method like :
33
+ # scope :context_as_follow , where(:context => follow)
34
+ cons_context.each do |cont|
35
+ method_name = "context_as_#{cont}".to_sym
36
+ scope method_name, where(:context => cont)
37
+ end
38
+
39
+ # Here add some dynamic scope query methods
40
+ # the method name is context + favorable_type(transfer it to a valid method string)
41
+ # for example,follow_investors retrive all record which context is follow and favorable type is investor
42
+ # sample output :
43
+ # scope :focus_tags , where(:favorable_type => "Tag",:context => "focus")
44
+ # This function seems not useful in real life
45
+ #
46
+ # cons_context_match.each do |context,favorable|
47
+ # favorable.each do |klass|
48
+ # method_name = "#{context}_#{klass.to_s.tableize.tr('/','_')}".to_sym
49
+ # scope method_name, where(:favorable_type => klass.to_s,:context => context.to_s)
50
+ # end
51
+ # end
52
+
53
+ # just used in test
54
+ # But in production, I think it should be run once at init
55
+ # write how to use it in readme.md
56
+ def self.context_match_keys_should_belongs_to_context
57
+ cons_context_match.keys.all? { |key| cons_context.include? key.to_s }
58
+ end
59
+ end
60
+
61
+
62
+ module ClassMethods
63
+
64
+ # Generate dynamic class methods like add_focus, add_follow
65
+ # Favor.add_focus has at least two parameters as add_focus(operator,resource)
66
+ # the operator is someone(maybe something) who has taken an action(context) on a resource
67
+ # the resource is target
68
+ # return an object with error when create favor failed
69
+ #
70
+ # Even method could receive a block to assign more attributes, but it is not recommended
71
+ # Favor is designed to handle simple data structure, mostly for simple relation data
72
+ # just the user click one button or touch one link, then a favor was created
73
+ def generate_add_methods(name)
74
+ method_name = "add_#{name}".to_sym
75
+ define_singleton_method method_name do |operator,resource,other_field_option={}|
76
+ build_hash = other_field_option.merge(context: name)
77
+ favor = operator.favors.build build_hash
78
+ favor.favorable = resource
79
+ yield favor if block_given?
80
+ favor.save ? true : favor # check rails create method to make this ok
81
+ end
82
+ end
83
+
84
+ # Generate dynamic class methods like del_focus, del_follow
85
+ # Most like add_focus, but raise errors when delete favor failed
86
+ # TODO: add exception object when raise errors or use the same raise of destroy
87
+ def generate_del_methods(name)
88
+ method_name = "del_#{name}".to_sym
89
+ define_singleton_method method_name do |ope,res,other_field_option={}|
90
+ query_hash = other_field_option.merge(context: name, favorable: res)
91
+ favor = ope.favors.where(query_hash)
92
+ if favor.present?
93
+ favor.first.destroy
94
+ else
95
+ false
96
+ # raise "delete favor failed,can not find favor object",caller
97
+ end
98
+ end
99
+ end
100
+
101
+ # seems just used for relations module
102
+ def host_klasses
103
+ attr = "host_klasses_for_#{name.underscore}".to_sym
104
+ respond_to?(attr) ? send(attr) : []
105
+ end
106
+
107
+ def favorable_klasses
108
+ attr = "favorable_klasses_for_#{name.underscore}".to_sym
109
+ respond_to?(attr) ? send(attr) : []
110
+ end
111
+
112
+ def favorable_to_media
113
+ :be_favors
114
+ end
115
+
116
+ def media_to_host
117
+ :host
118
+ end
119
+ end # end ClassMethods
120
+
121
+ private
122
+
123
+ def host_should_differ_favorable
124
+ if host_id == favorable_id and host_type == favorable_type
125
+ errors.add(:favorable_id," can not the same as the host_id when types of host and favorable are the same ")
126
+ end
127
+ end
128
+
129
+ def context_should_match_favorable_type
130
+ unless cons_context_match[context.to_sym].present? and
131
+ cons_context_match[context.to_sym].include? favorable_type.to_sym
132
+ errors.add(:context," and favorable type not included in context match")
133
+ end
134
+ end
135
+
136
+ # how to reslove two or more hook
137
+ def context_relation_hook
138
+ context_relation.each do |key,contexts|
139
+ if contexts.include? context.to_sym
140
+ other_context = contexts.select { |con| con != context.to_sym }.first
141
+ send("relation_#{key}_hook",other_context) if self.class.private_method_defined? "relation_#{key}_hook"
142
+ end
143
+ end
144
+ end
145
+
146
+ def relation_oppose_hook(other_context)
147
+ oppose_context = other_context
148
+ oppose_favor= Favor.where(:host => host, :context => oppose_context, :favorable => favorable)
149
+ oppose_favor.first.destroy if oppose_favor.presence
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,12 @@
1
+ class Array
2
+
3
+ # return a Hash
4
+ def each_relations_on(element,mark)
5
+ select {|e| e != element }.inject({}) do |memo,record|
6
+ memo.merge!({record => mark}) unless memo.keys.include? record
7
+ memo
8
+ end
9
+ end
10
+
11
+ end
12
+
@@ -0,0 +1,68 @@
1
+ module OneTouch
2
+ module AsFavorHost
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+
7
+ class_attribute :default_context_in_host, :instance_writer => false
8
+
9
+ if favor_class.respond_to? :host_relation_type and favor_class.host_relation_type == :polymorphic
10
+ scope :join_favors, joins(:favors.type(self))
11
+ else
12
+ scope :join_favors, joins(:favors)
13
+ end
14
+
15
+ scope :favorable_is, lambda { |sth| where(:favors => { :favorable => sth } ) }
16
+
17
+ # One module may not only host but also favorable, ex: User model
18
+ # when User as favorable, defined a method called context_as, so here define another method to distinct the former
19
+ scope :host_context_as, lambda { |context| where(:favors => { :context => context } ) }
20
+
21
+ if favor_class.respond_to? :host_relation_type # means favor class has load acts_as_favor
22
+ build_context_klass_methods
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+
28
+ # if favor_class.host_relation_type == :polymorphic, then this will be automated executed
29
+ def rewrite_join_favors
30
+ scope :join_favors, joins(:favors.type(self))
31
+ end
32
+
33
+ def favor_to(favorable,context=default_context_in_host)
34
+ join_favors.host_context_as(context).
35
+ favorable_is(favorable)
36
+ end
37
+
38
+ def after_load_acts_as_favor
39
+ build_context_klass_methods
40
+ end
41
+
42
+ def build_context_klass_methods
43
+ favor_class.cons_context_match.each do |context, klasses|
44
+ klasses.each do |klass|
45
+ method_name = "#{context}_#{klass.to_s.underscore.pluralize}".to_sym
46
+ define_method method_name do
47
+ klass.to_s.constantize.favored_by(self,context)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end # End ClassMethods
54
+
55
+ # find the favor to target with specific context
56
+ # return ARelation object
57
+ def find_favor(target,context)
58
+ favors.where(context: context,favorable: target)
59
+ end
60
+
61
+ # check favor exist or not
62
+ # params is context and target
63
+ def favor?(target,context)
64
+ find_favor(target,context).present? ? true : false
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ module OneTouch
2
+ module AsFavorable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :default_context_in_favorable, :instance_writer => false
7
+
8
+ scope :join_be_favors, joins(:be_favors.type(self))
9
+
10
+ scope :host_is, lambda { |someone| where(:be_favors => { :host => someone } ) }
11
+ scope :context_as, lambda { |context| where(:be_favors => { :context => context } ) if context.present? }
12
+ scope :group_by_favorable, group("#{favor_klass.downcase.pluralize}.favorable_id")
13
+ # scope :includes_favors_and_hosts, includes(:be_favors => :host)
14
+
15
+ end # included
16
+
17
+ module ClassMethods
18
+
19
+ def allstars(context=default_context_in_favorable)
20
+ join_be_favors.context_as(context).group_by_favorable.
21
+ select("count(*) as counter, #{self.table_name}.*").order("counter desc")
22
+ end
23
+
24
+ def favored_by(user,context = default_context_in_favorable)
25
+ join_be_favors.context_as(context).
26
+ host_is(user)
27
+ end
28
+
29
+ def unfavored_by(user,context = default_context_in_favorable)
30
+ favored_by_ids = favored_by(user,context).map(&:id).presence || [0]
31
+ where(:id.not_in => favored_by_ids)
32
+ end
33
+
34
+ end # ClassMethods
35
+
36
+ def favors_count(cont=default_context_in_favorable)
37
+ be_favors.context_as(cont).count
38
+ end
39
+
40
+ # Return host list
41
+ # same method to Host.favor_to, but different aspect
42
+ def favorers(host = default_host, context = default_context_in_favorable)
43
+ host.favor_to(self, context)
44
+ end
45
+
46
+ def favored_by?(ahost, context= default_context_in_favorable)
47
+ be_favors.where(:host => ahost, :context => context).present?
48
+ end
49
+
50
+ private
51
+
52
+ # Should be Overrided in favorable class
53
+ def default_host
54
+ ::User
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ module OneTouch
2
+ module HostRelation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+
10
+ # results like
11
+ # { [:focus, @tagA] => [ User1, User2, User3] , [:dislike, @tagB] => [ User2, User5 ] ... }
12
+ # or { [ nil, @tagA] => [ Article1, Post2, User3] , [:nil, @tagB] => [ User2, User5 ] ... }
13
+ # or { [[:Tag, 1], :focus] => [ [:User, 1], [:User, 2] ... ] } or
14
+ # simply is { [:Tag, :focus] => [ [:User, 1], [:User, 2] ... ] } or
15
+ # just as { 1 => [ [:User, 1], [:User, 2] ... ] }, 1 is the relation point
16
+ # { TagA => [ :focus, User1, User2, User3] , TagB => [ :like, User2, User5 ] }
17
+ def host_relations(to_media, media_to_host)
18
+ includes(to_media.to_sym => media_to_host.to_sym).inject({}) do |memo,favorable|
19
+ favorable.send(to_media).each do |favor|
20
+ favor.respond_to?(:context) ? (context = favor.context) : (context = nil)
21
+ combine_key = [context, favorable]
22
+
23
+ host = favor.send media_to_host
24
+ memo.keys.include?(combine_key) ? memo[combine_key] << host : memo[combine_key] = [host]
25
+ end
26
+ memo
27
+ end
28
+ end
29
+ end # End ClassMethods
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ module OneTouch
2
+ module Relations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # should add be_favors and host as a class attributes
7
+ class_attribute :favorable_klasses unless respond_to? :favorable_klasses
8
+ class_attribute :target_to_media, :media_to_host, :instance_writer => false
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # build all favorable_to_host_relation
14
+ # results should be like Tag.host_relations
15
+ def build_relations
16
+ favorable_klasses.inject({}) do |memo,klass|
17
+ memo.merge(klass.to_s.constantize.host_relations(target_to_media, media_to_host))
18
+ end
19
+ end
20
+
21
+ # match relation and relation_rules, if relation did not match any rule, return 0 point
22
+ # This methods can be override by user
23
+ def matching_mark(relation_key)
24
+ @rule ||= relation_rules
25
+ transfer_key = relation_key.map { |ele| ele.is_a?(ActiveRecord::Base) ? ele.class.name.to_sym : ele.to_sym }
26
+ @rule[transfer_key].presence || 0
27
+ end
28
+
29
+ # Transfer ([User1, User2, User3], mark 5) to { User1 => { User2 => 5, User3 => 5 } ...}
30
+ # and combine the origin relation
31
+ # the code is a little ugly
32
+ def array_hosts_to_hash(origin_relation,hosts,mark)
33
+ hosts.inject(origin_relation) do |memo,host|
34
+ each_relations= hosts.each_relations_on(host,mark)
35
+ # the old_relation and new_relation both are Hash
36
+ memo.merge!(host => each_relations) do |key,old_r,new_r|
37
+ old_r.merge!(new_r) do |key,old_mark,new_mark|
38
+ old_mark + new_mark
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Transfer favorable_to_host_relation to host_to_host_relations
45
+ # TODO: use fiber to improve performance
46
+ # TODO: No test for it
47
+ def host_relations
48
+ h_relation = {}
49
+ build_relations.each do |key, hosts|
50
+ h_relation = array_hosts_to_hash h_relation, hosts, matching_mark(key)
51
+ end
52
+ h_relation
53
+ end
54
+
55
+ # TODO: add this function
56
+ def favorable_relations
57
+ end
58
+
59
+ end # end ClassMethods
60
+
61
+ end
62
+ end
@@ -0,0 +1,14 @@
1
+ require 'rails'
2
+ require File.join(File.dirname(__FILE__), 'models/array') # maybe move it outside models folder
3
+
4
+ module OneTouch
5
+
6
+ class Railtie < ::Rails::Railtie
7
+ initializer 'one_touch' do |app|
8
+ ActiveSupport.on_load(:active_record) do
9
+ require File.join(File.dirname(__FILE__), 'models/active_record_extension')
10
+ ::ActiveRecord::Base.send :include, OneTouch::ActiveRecordExtension
11
+ end
12
+ end
13
+ end # end Railtie
14
+ end
@@ -0,0 +1,3 @@
1
+ module OneTouch
2
+ VERSION = "1.0.0"
3
+ end
data/lib/one_touch.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "one_touch/version"
2
+ require File.join(File.dirname(__FILE__), 'one_touch/railtie')
3
+ require File.join(File.dirname(__FILE__), 'one_touch/engine')
File without changes
data/one_touch.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "one_touch/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "one_touch"
7
+ s.version = OneTouch::VERSION
8
+ s.authors = ["raykin"]
9
+ s.email = ["raykincoldxiao@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{ One Touch is used for recording relation between resources }
12
+ s.description = %q{ One Touch makes one click operation simple }
13
+
14
+ s.rubyforge_project = "one_touch"
15
+
16
+ s.add_runtime_dependency 'rails', '~> 3.0'
17
+ #s.add_development_dependency 'rails', '~> 3.0'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ end
data/readme.md ADDED
@@ -0,0 +1,122 @@
1
+ ### Intro
2
+ In webapp, many data is generated by just one click like focus someone, dislike some post.This gem make it easy.
3
+
4
+ These one click data most are relation data and managed in one single model in this gem. So we can analyze the relation data easily.
5
+
6
+ ## Dependency
7
+ version ~> 1 support Rails3.0 and need meta_where
8
+ version ~> 2 will support Rails3.1 and need squeel
9
+
10
+ ## Usage
11
+ gem install one_touch
12
+
13
+ Suppose we have User, Post and Tag model.We also has a Favor model which record the relations between users or between user and post.Favor model could be anyother model.
14
+
15
+ Then we define:
16
+ ```ruby
17
+ class User; as_favor_host; as_favorable; end;
18
+
19
+ class Post; as_favorable; end;
20
+
21
+ class Tag; as_favorable; end;
22
+
23
+ # Make sure Favor has defined constants CONTEXT and CONTEXT_MATCH before acts_as_favor
24
+ # The Favor column depend on the relations you defined
25
+ # In this case, it would be id, user_id, favorable_id, favorable_type, context
26
+ # Don't forget context column
27
+ class Favor
28
+ CONTEXT = Set.new(%w[focus favorite follow agree oppose buy subscribe])
29
+ CONTEXT_MATCH = { :focus => [:Tag, :Post], :follow => [:User] }
30
+
31
+ acts_as_favor
32
+ end
33
+ # The real relations between them are:
34
+ # User has_many :favors, :class_name => "Favor", :as => :host
35
+ # Post has_many :be_favors, :class_name => "Favor", :as => :favorable
36
+ # Favor belongs_to :host, :class_name => "User"; belongs_to :favorable, :polymorphic => true
37
+ # This is default setting
38
+ ```ruby
39
+ You can define the relation by yourself.
40
+ ```ruby
41
+ class Favor
42
+ belongs_to :host, :polymorphic => true
43
+ belongs_to :favorable, :polymorphic => true
44
+ acts_as_favor :no_default_relations => true
45
+ end
46
+ # make sure the relation name are host and favorable, they can not be changed
47
+ ```ruby
48
+ Or you want Favor model to be another name, Bridge.
49
+ ```ruby
50
+ class User
51
+ as_favor_host :klass => "Bridge"
52
+ end
53
+ ```ruby
54
+ In short, there are four relation names can not be configured, favors, be_favors, host and favorable.
55
+ And if you change the Favor model name, make sure it consists in all applications.
56
+
57
+ ## Features
58
+ Favor.add_context and Favor.del_context can add or del favors. Ex:
59
+ ```ruby
60
+ # @host focus on @favorable
61
+ Favor.add_focus(@host, @favorable)
62
+ #=> success return true, fail return error @favor object
63
+ #=> could also
64
+
65
+ # @host remove focus on @favorable
66
+ Favor.del_focus(@host, @favorable)
67
+
68
+ ```ruby
69
+
70
+ ### Query Methods
71
+ The most useful methods would be: favor_to, favored_by, allstars. They are all class methods.
72
+ ```ruby
73
+ User.favor_to(@user1, :follow) #=> return guys(an ActiveRelation object) who follows @user1, so you can define fans
74
+ def fans
75
+ User.favor_to(self, :follow)
76
+ end
77
+ @user1.fans # => the same results
78
+
79
+ Tag.favored_by(@user1, :focus) #=> return tag that @use1 focused
80
+
81
+ Tag.allstars(:focus) #=> return tags with a count attribute which returns how many focus on it
82
+
83
+ # Dynamic methods context_favorable_pluralize_klass, Ex:
84
+ @user1.focus_tags
85
+ # => return the tags which was focused by @user1
86
+ @user1.favor?(@tag1, :focus) #=> return true if @user1 focus on @tag1
87
+ ```ruby
88
+ Actually the favor_to and favored_by are fundmental methods which many other methods based on
89
+
90
+ #### Analyze Relation Data
91
+
92
+ Use Favor.host_relations to get the relations of Users.
93
+ But first you should define the relation mark rule.
94
+ ```ruby
95
+ class Favor
96
+ def self.relation_rules
97
+ { [:agree, :Answer] => 3, [:follow, :User] => 2, [:focus, :Tag] => 1 }
98
+ end
99
+ end
100
+ ```ruby
101
+
102
+ The results like:
103
+ ```ruby
104
+ Favor.host_relations
105
+ # It a Hash, the first element is
106
+ # => @user1 => { @user2 => 100, @user3 => 50, .... }
107
+ ```ruby
108
+ This means @user1 is more close to @user2 than @user3.Maybe the answers that @user1 and @user2 both agree are more than the answers that @user1 and @user2 both agree.Actually it depends on your mark rule, but it really shows there must be more common betweens @user1 and @user2.
109
+ ##### Becare running host_relations is really time consumer, so run it in backend job and keep the results.
110
+
111
+ ### Why call it one touch
112
+ Because We create or delete data in one click. No Form Needed.
113
+
114
+ ### TODO
115
+ Change the dependency meta_where to squeel and make it support Rails3.1
116
+ Add cache to favored_by, favor_to
117
+ Add a generator to make install faster.
118
+ Add another gem to supply view and controller template
119
+
120
+ ### Abstract Requirement
121
+ Connect any two resource in one model.
122
+ I really want to implement this gem as more abstract requirement.That means four relation names can be configured, But's not easy.
data/spec/fake_app.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'active_record'
2
+ require 'action_controller/railtie'
3
+ require 'action_view/railtie'
4
+
5
+ # database
6
+ ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}}
7
+ ActiveRecord::Base.establish_connection('test')
8
+
9
+ # config
10
+ app = Class.new(Rails::Application)
11
+ #app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
12
+ #app.config.session_store :cookie_store, :key => "_myapp_session"
13
+ app.config.active_support.deprecation = :log
14
+ app.initialize!
15
+ #
16
+ ## routes
17
+ #app.routes.draw do
18
+ # resources :users
19
+ #end
20
+
21
+ class Favor < ActiveRecord::Base
22
+
23
+ # CONTEXT is an array of string and do not change the string to symbol
24
+ # CONTEXT_MATCH is all symbols, do not change it
25
+ CONTEXT = Set.new(%w[dig focus favorite follow dislike])
26
+ CONTEXT_MATCH = { :focus => [:Tag],:favorite => [:Tag],
27
+ :follow => [:User,:Investor], :dislike => [:User] }
28
+ CONTEXT_RELATION = { :oppose => [:follow, :dislike], :contain => [:favorite, :focus] }
29
+
30
+ # acts_as_favor # should run it after table has create
31
+
32
+ # belongs_to :host, :polymorphic => true
33
+ # belongs_to :favorable, :polymorphic => true
34
+
35
+ def self.relation_rules
36
+ { [:follow, :User] => 2, [:dislike, :User] => 2, [:focus, :Tag] => 1 }
37
+ end
38
+
39
+ end
40
+
41
+ class TestingFoo < ActiveRecord::Base
42
+ CONTEXT = Set.new(%w[follow dislike])
43
+ CONTEXT_MATCH = { :dislike => [:User] }
44
+ CONTEXT_RELATION = { :oppose => [:follow, :dislike] }
45
+ end
46
+
47
+ class Ask < ActiveRecord::Base
48
+ end
49
+ class User < ActiveRecord::Base
50
+ end
51
+ class Tag < ActiveRecord::Base
52
+ end
53
+ class Tagging < ActiveRecord::Base
54
+ end
55
+
56
+ #migrations
57
+ class CreateAllTables < ActiveRecord::Migration
58
+ def self.up
59
+ create_table(:favors) do |t|
60
+ t.references :host,:polymorphic => true # if user just need t.string user_id
61
+ t.references :favorable,:polymorphic => true
62
+ t.string :context
63
+ end
64
+ create_table(:users) {|t| t.string :email;t.string :name}
65
+ create_table(:tags) {|t| t.string :name}
66
+ #create_table(:readerships) {|t| t.integer :user_id; t.integer :book_id }
67
+ #create_table(:authorships) {|t| t.integer :user_id; t.integer :book_id }
68
+ end
69
+
70
+ def self.down
71
+ drop_table :favors
72
+ drop_table :users
73
+ drop_table :tags
74
+ end
75
+
76
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe OneTouch::ActsAsFavor do
4
+
5
+ include OneTouch::LoadTestData
6
+
7
+ context ActiveRecord::Base do
8
+ [:acts_as_favor,:as_favor_host,:as_favorable].each do |method_name|
9
+ it "should respond to #{method_name}" do
10
+ ActiveRecord::Base.should respond_to method_name
11
+ end
12
+ end
13
+ end
14
+
15
+ it "favorable class should has two elements: user and tag" do
16
+ Favor.favorable_klasses.count.should eq(2)
17
+ Favor.favorable_klasses.should include(:User,:Tag)
18
+ end
19
+
20
+ it "should check context against entity type when the context is invalid" do
21
+ afavor.update_attributes context: "error_context"
22
+ afavor.valid?.should be_false
23
+ end
24
+
25
+ it "should check context against entity type when the context is valid" do
26
+ afavor.update_attributes context: "follow"
27
+ afavor.valid?.should be_false
28
+ end
29
+
30
+ it "should save ok when context is favorite and essay is target resource" do
31
+ afavor.update_attributes context: "favorite"
32
+ afavor.valid?.should be_true
33
+ end
34
+
35
+ [:add_focus, :del_focus, :context_as_follow].each do |method_name|
36
+ it "should has an class method #{method_name}" do
37
+ Favor.should respond_to method_name
38
+ end
39
+ end
40
+
41
+ it "should add a focus to a tag " do
42
+ fly_favor = Favor.add_focus foo,fly_tag
43
+ fly_favor.should be_true
44
+ foo.favors.where(context: 'focus',favorable_id: fly_tag.id,favorable_type: 'Tag').first.should be_true
45
+ end
46
+
47
+ it " foo can not follow him self ,return an instance of Favor" do
48
+ Favor.add_follow(foo,foo).class.should eq(Favor)
49
+ # lambda {Favor.add_follow(foo,foo)}.should raise_error(ActiveRecord::RecordInvalid)
50
+ end
51
+
52
+ it " keys of context_match should belongs to context " do
53
+ Favor.context_match_keys_should_belongs_to_context.should be_true
54
+ end
55
+
56
+ context TestingFoo do
57
+ it "should not contain method host_relations" do
58
+ TestingFoo.should_not respond_to 'host_relations'
59
+ end
60
+ end
61
+
62
+ describe "all stars" do
63
+
64
+ before :each do
65
+ Favor.add_follow(tom,foo)
66
+ Favor.add_follow(bar,foo)
67
+ Favor.add_follow(ray,foo)
68
+ Favor.add_follow(ray,bar)
69
+ Favor.add_follow(tom,bar)
70
+ Favor.add_follow(foo,ray)
71
+ Favor.add_follow(foo,tom)
72
+ end
73
+
74
+ it "keys of #build_relations should contains ['follow',foo]" do
75
+ Favor.build_relations.keys.should include(['follow',foo])
76
+ end
77
+
78
+ it "relation ['follow',foo] get 2 mark " do
79
+ first_build_relation = Favor.build_relations.select { |k,v| k.last == foo && k.first == 'follow' }
80
+ first_build_relation.keys.should include(['follow',foo])
81
+ Favor.matching_mark(first_build_relation.keys.first).should eq(2)
82
+ end
83
+
84
+ it "host relations should be a complex nested hash" do
85
+ h_relations = Favor.host_relations
86
+ h_relations.is_a? Hash
87
+ h_relations.values.first.is_a? Hash
88
+ foo_relations = h_relations.select {|k,v| k == foo}
89
+ foo_relations.values.should eq([{}])
90
+ bar_relations = h_relations.select {|k,v| k == bar}
91
+ bar_relations.values.should eq([{ tom => 2, ray => 2}])
92
+ bar_to_tom = bar_relations.values.select {|k,v| k== tom}
93
+ tom_relations = h_relations.select {|k,v| k == tom}
94
+ tom_to_bar = tom_relations.values.select {|k,v| k== bar}
95
+ bar_to_tom.should eq(tom_to_bar)
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe Array do
4
+
5
+ it "#each_relations_on should return a relation hash" do
6
+ a=%w[foo bar tom]
7
+ a.each_relations_on("tom",5).should eq({"foo" => 5, "bar" => 5})
8
+ end
9
+
10
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+
3
+ describe OneTouch::AsFavorHost do
4
+
5
+ include OneTouch::LoadTestData
6
+ context "foo" do
7
+
8
+ [:focus_tags,:favor_klass].each do |method_name|
9
+ it "should response #{method_name}, and ActiveRecord::Base not response to it" do
10
+ foo.should respond_to method_name
11
+ ActiveRecord::Base.should_not respond_to method_name
12
+ end
13
+ end
14
+
15
+ it "should find a favor on afavor" do
16
+ foo.favor?(tag,"favorite").should be_true
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe OneTouch::AsFavorable do
4
+
5
+ include OneTouch::LoadTestData
6
+
7
+ context Tag do
8
+ it " should contain classmethod allstars" do
9
+ Tag.should respond_to(:allstars)
10
+ end
11
+
12
+ it "when context is nil, return all" do
13
+ all_tags = Tag.all
14
+ Tag.context_as(nil).should eq(all_tags)
15
+ end
16
+
17
+ it "first tag favors count should be 1" do
18
+ Tag.first.favors_count.should eq(1)
19
+ end
20
+
21
+ end
22
+
23
+ context "first tag" do
24
+ it " favored_by? foo is true " do
25
+ Tag.first.favored_by?(foo, :favorite).should be_true
26
+ end
27
+ end
28
+
29
+ context "User" do
30
+
31
+ before :each do
32
+ Favor.add_follow(tom,foo)
33
+ Favor.add_follow(bar,foo)
34
+ Favor.add_follow(ray,foo)
35
+ Favor.add_follow(ray,bar)
36
+ Favor.add_follow(tom,bar)
37
+ Favor.add_follow(foo,ray)
38
+ Favor.add_follow(foo,tom)
39
+ end
40
+
41
+ let(:foo_follows) { User.favored_by(foo, "follow") }
42
+
43
+ context Favor do
44
+ it "build relations should return a hash of relation between favorable and host" do
45
+ Favor.build_relations.instance_of?(Hash).should be_true
46
+ Favor.build_relations.first.first.should eq(["favorite", Tag.first]) # means first key
47
+ end
48
+
49
+ end
50
+
51
+ it "host relations should has key [foo,'follow'] " do
52
+ User.host_relations(:be_favors, :host).keys.should include(['follow', foo])
53
+ end
54
+
55
+ it "host relations of foo follow should be tom, bar, ray" do
56
+ User.host_relations(:be_favors, :host)[['follow',foo]].should eq([tom,bar,ray])
57
+ end
58
+
59
+ it "allstars of be followed should has four guy,first one is foo,second is bar" do
60
+ allstars = User.allstars(:follow)
61
+ # allstars.count.should == 4 # it failed cause allstars.to_sql has a count(*)
62
+ allstars.all.count.should == 4
63
+ allstars.first.should eq(foo)
64
+ allstars.second.should eq(bar)
65
+ allstars.first.counter.should eq(3)
66
+ end
67
+
68
+ it "favored by foo should be ray and tom" do
69
+ foo_follows.first.should eq(ray)
70
+ foo_follows.second.should eq(tom)
71
+ end
72
+
73
+ # favor_to is defined in as_favor_host.rb
74
+ it "foo's fans should contains three guys that are tom,bar,ray" do
75
+ fans= User.favor_to(foo, "follow")
76
+ fans.all.count.should eq(3)
77
+ fans.should include(tom,ray,bar)
78
+ end
79
+
80
+ it "favorers of foo should be three guys " do
81
+ favorers = foo.favorers(User,:follow)
82
+ favorers.count.should eq(3)
83
+ favorers.should include(tom,ray,bar)
84
+ end
85
+
86
+ # follow_users is defined in as_favor_host.rb
87
+ it "foo's follow_users should contains two guys as ray,tom" do
88
+ follows= foo.follow_users
89
+ follows.count.should eq(2)
90
+ follows.should include(ray,tom)
91
+ end
92
+
93
+ it " when foo dislike ray, foo's followings did not include ray " do
94
+ Favor.add_dislike(foo,ray)
95
+ foo_follows = User.favored_by(foo, "follow")
96
+ foo_follows.count.should eq(1)
97
+ foo_follows.should include(tom)
98
+ no_foo_follows = User.unfavored_by(foo, "follow")
99
+ no_foo_follows.count.should eq(User.count - 1)
100
+ end
101
+
102
+ it " unfollow singleone is all peoples " do
103
+ single_one = User.create(email: "singleone@test.com")
104
+ User.unfavored_by(single_one, 'follow').count.should eq(User.count)
105
+ end
106
+
107
+ end
108
+
109
+ end
@@ -0,0 +1,17 @@
1
+ module OneTouch
2
+ module LoadTestData
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ let!(:foo) { User.create(email: "foo@test.com",name: "iamfoo") }
7
+ let!(:bar) { User.create(email: "bar@test.com") }
8
+ let!(:ray) { User.create(email: "ray@test.com") }
9
+ let!(:tom) { User.create(email: "tom@test.com") }
10
+ let!(:tag) { Tag.create :name => "money" }
11
+ let!(:fly_tag) { Tag.create :name => "fly" }
12
+ let!(:afavor) { Favor.create :host_id => foo.id, :host_type => "User", :favorable_id => tag.id,:favorable_type => "Tag" , :context => "favorite" }
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rails'
4
+ require 'one_touch'
5
+ require File.join(File.dirname(__FILE__), 'fake_app')
6
+ require 'meta_where'
7
+ #require 'squeel'
8
+ require 'rspec/rails'
9
+ require 'models/load_test_data'
10
+
11
+
12
+ RSpec.configure do |config|
13
+ config.use_transactional_fixtures = true
14
+
15
+ config.before :all do
16
+ #CreateAllTables.down if ActiveRecord::Base.connection.table_exists? 'favors'
17
+ CreateAllTables.up unless ActiveRecord::Base.connection.table_exists? 'favors'
18
+
19
+ # Squeel.configure do |config|
20
+ # config.load_core_extensions :hash, :symbol
21
+ # end
22
+
23
+ Tag.as_favorable
24
+
25
+ User.as_favor_host
26
+
27
+ User.as_favorable
28
+ Favor.acts_as_favor :host_class_name => nil
29
+ # TestingFoo.acts_as_favor
30
+ # Tag.as_favorable :be_tagging_favors, :klass => "Tagging"
31
+ # Ask.as_favor_host :tagging_favors, :klass => "Tagging"
32
+ # Tagging.acts_as_favor :host_class_name => nil, :favorable_klass_name => :Tag, :include_favor_relations_module => true
33
+ # relation should be in another module
34
+ end
35
+ end
36
+
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: one_touch
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - raykin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-11 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &25065280 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *25065280
25
+ description: ! ' One Touch makes one click operation simple '
26
+ email:
27
+ - raykincoldxiao@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - Rakefile
35
+ - lib/one_touch.rb
36
+ - lib/one_touch/engine.rb
37
+ - lib/one_touch/models/active_record_extension.rb
38
+ - lib/one_touch/models/acts_as_favor.rb
39
+ - lib/one_touch/models/array.rb
40
+ - lib/one_touch/models/as_favor_host.rb
41
+ - lib/one_touch/models/as_favorable.rb
42
+ - lib/one_touch/models/host_relation.rb
43
+ - lib/one_touch/models/relations.rb
44
+ - lib/one_touch/railtie.rb
45
+ - lib/one_touch/version.rb
46
+ - log/development.log
47
+ - one_touch.gemspec
48
+ - readme.md
49
+ - spec/fake_app.rb
50
+ - spec/models/acts_as_favor_spec.rb
51
+ - spec/models/array_spec.rb
52
+ - spec/models/as_favor_host_spec.rb
53
+ - spec/models/as_favorable_spec.rb
54
+ - spec/models/load_test_data.rb
55
+ - spec/spec_helper.rb
56
+ homepage: ''
57
+ licenses: []
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project: one_touch
76
+ rubygems_version: 1.8.11
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: One Touch is used for recording relation between resources
80
+ test_files: []
81
+ has_rdoc: