acts-as-taggable-on-padrino 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.rspec +2 -0
  2. data/CHANGELOG +29 -0
  3. data/Gemfile +15 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +194 -0
  6. data/Rakefile +33 -0
  7. data/VERSION +1 -0
  8. data/acts-as-taggable-on-padrino.gemspec +94 -0
  9. data/lib/acts-as-taggable-on-padrino.rb +41 -0
  10. data/lib/acts_as_taggable_on_padrino/tag.rb +72 -0
  11. data/lib/acts_as_taggable_on_padrino/taggable.rb +64 -0
  12. data/lib/acts_as_taggable_on_padrino/taggable/cache.rb +54 -0
  13. data/lib/acts_as_taggable_on_padrino/taggable/collection.rb +91 -0
  14. data/lib/acts_as_taggable_on_padrino/taggable/core.rb +244 -0
  15. data/lib/acts_as_taggable_on_padrino/taggable/ownership.rb +104 -0
  16. data/lib/acts_as_taggable_on_padrino/taggable/related.rb +60 -0
  17. data/lib/acts_as_taggable_on_padrino/taggable/tag_list.rb +96 -0
  18. data/lib/acts_as_taggable_on_padrino/tagger.rb +65 -0
  19. data/lib/acts_as_taggable_on_padrino/tagging.rb +29 -0
  20. data/lib/acts_as_taggable_on_padrino/tags_helper.rb +17 -0
  21. data/lib/tasks/generate_migration.rb +23 -0
  22. data/lib/tasks/templates/migration.rb +29 -0
  23. data/spec/acts_as_taggable_on_padrino/acts_as_taggable_on_spec.rb +263 -0
  24. data/spec/acts_as_taggable_on_padrino/acts_as_tagger_spec.rb +110 -0
  25. data/spec/acts_as_taggable_on_padrino/tag_list_spec.rb +70 -0
  26. data/spec/acts_as_taggable_on_padrino/tag_spec.rb +105 -0
  27. data/spec/acts_as_taggable_on_padrino/taggable_spec.rb +333 -0
  28. data/spec/acts_as_taggable_on_padrino/tagger_spec.rb +90 -0
  29. data/spec/acts_as_taggable_on_padrino/tagging_spec.rb +26 -0
  30. data/spec/acts_as_taggable_on_padrino/tags_helper_spec.rb +26 -0
  31. data/spec/bm.rb +52 -0
  32. data/spec/database.yml.sample +17 -0
  33. data/spec/models.rb +47 -0
  34. data/spec/schema.rb +57 -0
  35. data/spec/spec_helper.rb +60 -0
  36. data/uninstall.rb +1 -0
  37. metadata +175 -0
@@ -0,0 +1,64 @@
1
+ module ActsAsTaggableOn
2
+ module Taggable
3
+ def taggable?
4
+ false
5
+ end
6
+
7
+ ##
8
+ # This is an alias for calling <tt>acts_as_taggable_on_padrino :tags</tt>.
9
+ #
10
+ # Example:
11
+ # class Book < ActiveRecord::Base
12
+ # acts_as_taggable
13
+ # end
14
+ def acts_as_taggable(opts = {})
15
+ acts_as_taggable_on :tags, opts
16
+ end
17
+
18
+ ##
19
+ # Make a model taggable on specified contexts.
20
+ #
21
+ # @param [Array] tag_types An array of taggable contexts
22
+ #
23
+ # Example:
24
+ # class User < ActiveRecord::Base
25
+ # acts_as_taggable_on_padrino :languages, :skills
26
+ # end
27
+ def acts_as_taggable_on(*tag_types)
28
+ opts = tag_types.extract_options!
29
+ opts.assert_valid_keys :tag, :tagging
30
+
31
+ tag_types = tag_types.to_a.flatten.compact.map {|type| type.to_sym }
32
+
33
+ if taggable?
34
+ write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
35
+ else
36
+ opts.reverse_merge!(:tag => 'Tag', :tagging => 'Tagging')
37
+ tag_class_name = opts[:tag]
38
+ tagging_class_name = opts[:tagging]
39
+ tag = tag_class_name.constantize
40
+ tagging = tagging_class_name.constantize
41
+
42
+ write_inheritable_attribute(:tag_types, tag_types)
43
+ write_inheritable_attribute(:acts_as_taggable_on_tag_model, tag)
44
+ write_inheritable_attribute(:acts_as_taggable_on_tagging_model, tagging)
45
+ class_inheritable_reader(:tag_types, :acts_as_taggable_on_tagging_model, :acts_as_taggable_on_tag_model)
46
+
47
+ class_eval do
48
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => tagging_class_name
49
+ has_many :base_tags, :through => :taggings, :source => :tag, :class_name => tag_class_name
50
+
51
+ def self.taggable?
52
+ true
53
+ end
54
+
55
+ include ActsAsTaggableOn::Taggable::Core
56
+ include ActsAsTaggableOn::Taggable::Collection
57
+ include ActsAsTaggableOn::Taggable::Cache
58
+ include ActsAsTaggableOn::Taggable::Ownership
59
+ include ActsAsTaggableOn::Taggable::Related
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Cache
3
+ def self.included(base)
4
+ # Skip adding caching capabilities if table not exists or no cache columns exist
5
+ return unless base.table_exists? && base.tag_types.any? { |context| base.column_names.include?("cached_#{context.to_s.singularize}_list") }
6
+
7
+ base.send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
8
+ base.extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
9
+
10
+ base.class_eval do
11
+ before_save :save_cached_tag_list
12
+ end
13
+
14
+ base.initialize_acts_as_taggable_on_cache
15
+ end
16
+
17
+ module ClassMethods
18
+ def initialize_acts_as_taggable_on_cache
19
+ tag_types.each do |tag_type|
20
+ class_eval %(
21
+ def self.caching_#{tag_type.to_s.singularize}_list?
22
+ caching_tag_list_on?("#{tag_type}")
23
+ end
24
+ )
25
+ end
26
+ end
27
+
28
+ def acts_as_taggable_on(*args)
29
+ super
30
+ initialize_acts_as_taggable_on_cache
31
+ end
32
+
33
+ def caching_tag_list_on?(context)
34
+ column_names.include?("cached_#{context.to_s.singularize}_list")
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ def save_cached_tag_list
40
+ tag_types.each do |tag_type|
41
+ tag_type = tag_type.to_s.singularize
42
+ if self.class.send("caching_#{tag_type}_list?")
43
+ if tag_list_cache_set_on(tag_type)
44
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
45
+ self["cached_#{tag_type}_list"] = list
46
+ end
47
+ end
48
+ end
49
+
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,91 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Collection
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
6
+ base.initialize_acts_as_taggable_on_collection
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_collection
11
+ tag_types.each do |tag_type|
12
+ singular_tag_type = tag_type.to_s.singularize
13
+ class_eval %(
14
+ def self.#{singular_tag_type}_counts(options={})
15
+ tag_counts_on('#{tag_type}', options)
16
+ end
17
+
18
+ def #{singular_tag_type}_counts(options = {})
19
+ tag_counts_on('#{tag_type}', options)
20
+ end
21
+
22
+ def top_#{tag_type}(limit = 10)
23
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
24
+ end
25
+
26
+ def self.top_#{tag_type}(limit = 10)
27
+ tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
28
+ end
29
+ )
30
+ end
31
+ end
32
+
33
+ def acts_as_taggable_on(*args)
34
+ super
35
+ initialize_acts_as_taggable_on_collection
36
+ end
37
+
38
+ def tag_counts_on(context, options = {})
39
+ all_tag_counts(options.merge({:on => context.to_s}))
40
+ end
41
+
42
+ ##
43
+ # Calculate the tag counts for all tags.
44
+ #
45
+ # @param [Hash] options Options:
46
+ # * :start_at - Restrict the tags to those created after a certain time
47
+ # * :end_at - Restrict the tags to those created before a certain time
48
+ # * :conditions - A piece of SQL conditions to add to the query
49
+ # * :limit - The maximum number of tags to return
50
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
51
+ # * :at_least - Exclude tags with a frequency less than the given value
52
+ # * :at_most - Exclude tags with a frequency greater than the given value
53
+ # * :on - Scope the find to only include a certain context
54
+ def all_tag_counts(options = {})
55
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
56
+
57
+ ## Generate scope:
58
+ tagging_scope = acts_as_taggable_on_tagging_model.select("#{acts_as_taggable_on_tagging_model.table_name}.tag_id, COUNT(#{acts_as_taggable_on_tagging_model.table_name}.tag_id) AS tags_count").
59
+ joins("INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{acts_as_taggable_on_tagging_model.table_name}.taggable_id").
60
+ where(:taggable_type => base_class.name)
61
+ tagging_scope = tagging_scope.where(table_name => {inheritance_column => name}) unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
62
+ tagging_scope = tagging_scope.where(:taggable_id => options.delete(:id)) if options[:id]
63
+ tagging_scope = tagging_scope.where(:context => options.delete(:on).to_s) if options[:on]
64
+ tagging_scope = tagging_scope.where(["#{acts_as_taggable_on_tagging_model.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
65
+ tagging_scope = tagging_scope.where(["#{acts_as_taggable_on_tagging_model.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
66
+
67
+ tag_scope = acts_as_taggable_on_tag_model.select("#{acts_as_taggable_on_tag_model.table_name}.*, #{acts_as_taggable_on_tagging_model.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
68
+ tag_scope.where(options[:conditions]) if options[:conditions]
69
+
70
+ # GROUP BY and HAVING clauses:
71
+ at_least = sanitize_sql(['tags_count >= ?', options.delete(:at_least)]) if options[:at_least]
72
+ at_most = sanitize_sql(['tags_count <= ?', options.delete(:at_most)]) if options[:at_most]
73
+ having = ["COUNT(#{acts_as_taggable_on_tagging_model.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
74
+
75
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
76
+ tagging_scope = tagging_scope.where(:taggable_id => select("#{table_name}.#{primary_key}")).
77
+ group("#{acts_as_taggable_on_tagging_model.table_name}.tag_id").
78
+ having(having)
79
+
80
+ tag_scope = tag_scope.joins("JOIN (#{tagging_scope.to_sql}) AS taggings ON taggings.tag_id = tags.id")
81
+ tag_scope
82
+ end
83
+ end
84
+
85
+ module InstanceMethods
86
+ def tag_counts_on(context, options={})
87
+ self.class.tag_counts_on(context, options.merge(:id => id))
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,244 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Core
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
6
+
7
+ base.class_eval do
8
+ attr_writer :custom_contexts
9
+ after_save :save_tags
10
+ end
11
+
12
+ base.initialize_acts_as_taggable_on_core
13
+ end
14
+
15
+ module ClassMethods
16
+ def initialize_acts_as_taggable_on_core
17
+ tag_types.each do |tags_type|
18
+ tag_type = tags_type.to_s.singularize
19
+ context_taggings = "#{tag_type}_taggings".to_sym
20
+ context_tags = tags_type.to_sym
21
+
22
+ class_eval do
23
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => acts_as_taggable_on_tagging_model.name,
24
+ :conditions => ["#{acts_as_taggable_on_tagging_model.table_name}.tag_id = #{acts_as_taggable_on_tag_model.table_name}.id AND #{acts_as_taggable_on_tagging_model.table_name}.context = ?", tags_type]
25
+ has_many context_tags, :through => context_taggings, :source => :tag, :class_name => acts_as_taggable_on_tag_model.name
26
+ end
27
+
28
+ class_eval %(
29
+ def #{tag_type}_list
30
+ tag_list_on('#{tags_type}')
31
+ end
32
+
33
+ def #{tag_type}_list=(new_tags)
34
+ set_tag_list_on('#{tags_type}', new_tags)
35
+ end
36
+
37
+ def all_#{tags_type}_list
38
+ all_tags_list_on('#{tags_type}')
39
+ end
40
+ )
41
+ end
42
+ end
43
+
44
+ def acts_as_taggable_on(*args)
45
+ super
46
+ initialize_acts_as_taggable_on_core
47
+ end
48
+
49
+ # all column names are necessary for PostgreSQL group clause
50
+ def grouped_column_names_for(object)
51
+ if object.connection.adapter_name == 'PostgreSQL'
52
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
53
+ else
54
+ "#{object.table_name}.#{object.primary_key}"
55
+ end
56
+ end
57
+
58
+ ##
59
+ # Return a scope of objects that are tagged with the specified tags.
60
+ #
61
+ # @param tags The tags that we want to query for
62
+ # @param [Hash] options A hash of options to alter you query:
63
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
64
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
65
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
66
+ #
67
+ # Example:
68
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
69
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
70
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
71
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
72
+ def tagged_with(tags, options = {})
73
+ tag_list = ActsAsTaggableOn::Taggable::TagList.from(tags)
74
+
75
+ return {} if tag_list.empty?
76
+
77
+ joins = []
78
+ conditions = []
79
+
80
+ context = options.delete(:on)
81
+
82
+ if options.delete(:exclude)
83
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{acts_as_taggable_on_tag_model.table_name}.name #{ActsAsTaggableOn.like_operator} ?", t]) }.join(" OR ")
84
+ conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{acts_as_taggable_on_tagging_model.table_name}.taggable_id FROM #{acts_as_taggable_on_tagging_model.table_name} JOIN #{acts_as_taggable_on_tag_model.table_name} ON #{acts_as_taggable_on_tagging_model.table_name}.tag_id = #{acts_as_taggable_on_tag_model.table_name}.id AND (#{tags_conditions}) WHERE #{acts_as_taggable_on_tagging_model.table_name}.taggable_type = #{quote_value(base_class.name)})"
85
+
86
+ elsif options.delete(:any)
87
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{acts_as_taggable_on_tag_model.table_name}.name #{ActsAsTaggableOn.like_operator} ?", t]) }.join(" OR ")
88
+ conditions << "#{table_name}.#{primary_key} IN (SELECT #{acts_as_taggable_on_tagging_model.table_name}.taggable_id FROM #{acts_as_taggable_on_tagging_model.table_name} JOIN #{acts_as_taggable_on_tag_model.table_name} ON #{acts_as_taggable_on_tagging_model.table_name}.tag_id = #{acts_as_taggable_on_tag_model.table_name}.id AND (#{tags_conditions}) WHERE #{acts_as_taggable_on_tagging_model.table_name}.taggable_type = #{quote_value(base_class.name)})"
89
+
90
+ else
91
+ tags = acts_as_taggable_on_tag_model.named_any(tag_list)
92
+ return where("1 = 0") unless tags.length == tag_list.length
93
+
94
+ tags.each do |tag|
95
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
96
+ prefix = "#{safe_tag}_#{rand(1024)}"
97
+
98
+ taggings_alias = "#{undecorated_table_name}_taggings_#{prefix}"
99
+
100
+ tagging_join = "JOIN #{acts_as_taggable_on_tagging_model.table_name} #{taggings_alias}" +
101
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
102
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
103
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
104
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
105
+
106
+ joins << tagging_join
107
+ end
108
+ end
109
+
110
+ taggings_alias, tags_alias = "#{undecorated_table_name}_taggings_group", "#{undecorated_table_name}_tags_group"
111
+
112
+ if options.delete(:match_all)
113
+ joins << "LEFT OUTER JOIN #{acts_as_taggable_on_tagging_model.table_name} #{taggings_alias}" +
114
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
115
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
116
+
117
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
118
+ end
119
+
120
+
121
+ where(conditions.join(" AND ")).
122
+ joins(joins.join(" ")).
123
+ group(group).
124
+ order(options[:order]).
125
+ readonly(false)
126
+ end
127
+
128
+ def is_taggable?
129
+ true
130
+ end
131
+ end
132
+
133
+ module InstanceMethods
134
+ # all column names are necessary for PostgreSQL group clause
135
+ def grouped_column_names_for(object)
136
+ self.class.grouped_column_names_for(object)
137
+ end
138
+
139
+ def custom_contexts
140
+ @custom_contexts ||= []
141
+ end
142
+
143
+ def is_taggable?
144
+ self.class.is_taggable?
145
+ end
146
+
147
+ def add_custom_context(value)
148
+ value = value.to_s
149
+ custom_contexts << value unless tagging_contexts.include?(value)
150
+ end
151
+
152
+ def cached_tag_list_on(context)
153
+ self["cached_#{context.to_s.singularize}_list"]
154
+ end
155
+
156
+ def tag_list_cache_set_on(context)
157
+ variable_name = "@#{context.to_s.singularize}_list"
158
+ !instance_variable_get(variable_name).nil?
159
+ end
160
+
161
+ def tag_list_cache_on(context)
162
+ variable_name = "@#{context.to_s.singularize}_list"
163
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::Taggable::TagList.new(tags_on(context).names))
164
+ end
165
+
166
+ def tag_list_on(context)
167
+ add_custom_context(context)
168
+ tag_list_cache_on(context)
169
+ end
170
+
171
+ def all_tags_list_on(context)
172
+ variable_name = "@all_#{context.to_s.singularize}_list"
173
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
174
+
175
+ instance_variable_set(variable_name, ActsAsTaggableOn::Taggable::TagList.new(all_tags_on(context).names).freeze)
176
+ end
177
+
178
+ ##
179
+ # Returns all tags of a given context
180
+ def all_tags_on(context)
181
+ base_tags.where(:taggings => {:context => context.to_s}).
182
+ group(grouped_column_names_for(acts_as_taggable_on_tag_model)).
183
+ order("max(#{acts_as_taggable_on_tagging_model.table_name}.created_at)")
184
+ end
185
+
186
+ ##
187
+ # Returns all tags that are not owned of a given context
188
+ def tags_on(context)
189
+ base_tags.where(["#{acts_as_taggable_on_tagging_model.table_name}.context = ? AND #{acts_as_taggable_on_tagging_model.table_name}.tagger_id IS NULL", context.to_s])
190
+ end
191
+
192
+ def set_tag_list_on(context, new_list)
193
+ add_custom_context(context)
194
+
195
+ variable_name = "@#{context.to_s.singularize}_list"
196
+ instance_variable_set(variable_name, ActsAsTaggableOn::Taggable::TagList.from(new_list))
197
+ end
198
+
199
+ def tagging_contexts
200
+ custom_contexts + self.class.tag_types.map {|type| type.to_s }
201
+ end
202
+
203
+ def reload(*args)
204
+ self.class.tag_types.each do |context|
205
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
206
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
207
+ end
208
+
209
+ super
210
+ end
211
+
212
+ def save_tags
213
+ tagging_contexts.each do |context|
214
+ next unless tag_list_cache_set_on(context)
215
+
216
+ tag_list = tag_list_cache_on(context).uniq
217
+
218
+ # Find existing tags or create non-existing tags:
219
+ tag_list = acts_as_taggable_on_tag_model.find_or_create_all_with_like_by_name(tag_list)
220
+
221
+ current_tags = tags_on(context)
222
+ old_tags = current_tags - tag_list
223
+ new_tags = tag_list - current_tags
224
+
225
+ # Find taggings to remove:
226
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
227
+ :context => context.to_s, :tag_id => old_tags)
228
+
229
+ if old_taggings.present?
230
+ # Destroy old taggings:
231
+ acts_as_taggable_on_tagging_model.destroy_all :id => old_taggings.map {|tagging| tagging.id }
232
+ end
233
+
234
+ # Create new taggings:
235
+ new_tags.each do |tag|
236
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
237
+ end
238
+ end
239
+
240
+ true
241
+ end
242
+ end
243
+ end
244
+ end