one_touch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: