tagtical 1.0.6

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.
Files changed (41) hide show
  1. data/CHANGELOG +25 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +25 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +306 -0
  6. data/Rakefile +59 -0
  7. data/VERSION +1 -0
  8. data/generators/tagtical_migration/tagtical_migration_generator.rb +7 -0
  9. data/generators/tagtical_migration/templates/migration.rb +34 -0
  10. data/lib/generators/tagtical/migration/migration_generator.rb +32 -0
  11. data/lib/generators/tagtical/migration/templates/active_record/migration.rb +35 -0
  12. data/lib/tagtical/acts_as_tagger.rb +69 -0
  13. data/lib/tagtical/compatibility/Gemfile +8 -0
  14. data/lib/tagtical/compatibility/active_record_backports.rb +21 -0
  15. data/lib/tagtical/tag.rb +314 -0
  16. data/lib/tagtical/tag_list.rb +133 -0
  17. data/lib/tagtical/taggable/cache.rb +53 -0
  18. data/lib/tagtical/taggable/collection.rb +141 -0
  19. data/lib/tagtical/taggable/core.rb +317 -0
  20. data/lib/tagtical/taggable/ownership.rb +110 -0
  21. data/lib/tagtical/taggable/related.rb +60 -0
  22. data/lib/tagtical/taggable.rb +51 -0
  23. data/lib/tagtical/tagging.rb +42 -0
  24. data/lib/tagtical/tags_helper.rb +17 -0
  25. data/lib/tagtical.rb +47 -0
  26. data/rails/init.rb +1 -0
  27. data/spec/bm.rb +53 -0
  28. data/spec/database.yml +17 -0
  29. data/spec/database.yml.sample +17 -0
  30. data/spec/models.rb +60 -0
  31. data/spec/schema.rb +46 -0
  32. data/spec/spec_helper.rb +159 -0
  33. data/spec/tagtical/acts_as_tagger_spec.rb +94 -0
  34. data/spec/tagtical/tag_list_spec.rb +102 -0
  35. data/spec/tagtical/tag_spec.rb +301 -0
  36. data/spec/tagtical/taggable_spec.rb +460 -0
  37. data/spec/tagtical/tagger_spec.rb +76 -0
  38. data/spec/tagtical/tagging_spec.rb +52 -0
  39. data/spec/tagtical/tags_helper_spec.rb +28 -0
  40. data/spec/tagtical/tagtical_spec.rb +340 -0
  41. metadata +132 -0
@@ -0,0 +1,317 @@
1
+ module Tagtical::Taggable
2
+ module Core
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include Tagtical::Taggable::Core::InstanceMethods
6
+ extend Tagtical::Taggable::Core::ClassMethods
7
+
8
+ after_save :save_tags
9
+
10
+ initialize_tagtical_core
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def initialize_tagtical_core
16
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagtical::Tagging"
17
+ has_many :tags, :through => :taggings, :source => :tag, :class_name => "Tagtical::Tag",
18
+ :select => "#{Tagtical::Tag.table_name}.*, #{Tagtical::Tagging.table_name}.relevance" # include the relevance on the tags
19
+
20
+ tag_types.each do |tag_type| # has_many :tags gets created here
21
+
22
+ # Aryk: Instead of defined multiple associations for the different types of tags, I decided
23
+ # to define the main associations (tags and taggings) and use AR scope's to build off of them.
24
+ # This keeps your reflections cleaner.
25
+
26
+ # In the case of the base tag type, it will just use the :tags association defined above.
27
+ Tagtical::Tag.scope(tag_type.scope_name, Proc.new { |options| tag_type.scoping(options || {}) }) unless Tagtical::Tag.respond_to?(tag_type.scope_name)
28
+
29
+ # If the tag_type is base? (type=="tag"), then we add additional functionality to the AR
30
+ # has_many :tags.
31
+ #
32
+ # taggable_model.tags(:only => :children)
33
+ # taggable_model.tags <-- still works like normal has_many
34
+ # taggable_model.tags(true, :only => :current) <-- reloads the tags association and appends scope for only current type.
35
+ if tag_type.has_many_name==:tags
36
+ define_method("tags_with_finder_type_options") do |*args|
37
+ options = args.pop if args.last.is_a?(Hash)
38
+ scope = tags_without_finder_type_options(*args)
39
+ options ? scope.tags(options) : scope
40
+ end
41
+ alias_method_chain :tags, :finder_type_options
42
+ else
43
+ delegate tag_type.has_many_name, :to => :tags
44
+ end
45
+
46
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
+ def self.with_#{tag_type.pluralize}(*tags)
48
+ options = tags.extract_options!
49
+ tagged_with(tags.flatten, options.merge(:on => :#{tag_type}))
50
+ end
51
+
52
+ def #{tag_type}_list
53
+ tag_list_on('#{tag_type}')
54
+ end
55
+
56
+ def #{tag_type}_list=(new_tags)
57
+ set_tag_list_on('#{tag_type}', new_tags)
58
+ end
59
+
60
+ def all_#{tag_type.pluralize}_list
61
+ all_tags_list_on('#{tag_type}')
62
+ end
63
+ RUBY
64
+
65
+ end
66
+ end
67
+
68
+ def acts_as_taggable(*args)
69
+ super(*args)
70
+ initialize_tagtical_core
71
+ end
72
+
73
+ # all column names are necessary for PostgreSQL group clause
74
+ def grouped_column_names_for(object)
75
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
76
+ end
77
+
78
+ ##
79
+ # Return a scope of objects that are tagged with the specified tags.
80
+ #
81
+ # @param tags The tags that we want to query for
82
+ # @param [Hash] options A hash of options to alter you query:
83
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
84
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
85
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
86
+ #
87
+ # Example:
88
+ # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
89
+ # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
90
+ # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
91
+ # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
92
+ # User.tagged_with("awesome", "cool", :on => :skills) # Users that are tagged with just awesome and cool on skills
93
+ def tagged_with(tags, options = {})
94
+ tag_list = Tagtical::TagList.from(tags)
95
+ return scoped(:conditions => "1 = 0") if tag_list.empty? && !options[:exclude]
96
+
97
+ joins = []
98
+ conditions = []
99
+
100
+ options[:on] ||= Tagtical::Tag::Type::BASE
101
+ tag_type = Tagtical::Tag::Type.find(options.delete(:on))
102
+ finder_type_condition_options = options.extract!(:only)
103
+
104
+ tag_table, tagging_table = Tagtical::Tag.table_name, Tagtical::Tagging.table_name
105
+
106
+ if options.delete(:exclude)
107
+ conditions << "#{table_name}.#{primary_key} NOT IN (" +
108
+ "SELECT #{tagging_table}.taggable_id " +
109
+ "FROM #{tagging_table} " +
110
+ "JOIN #{tag_table} ON #{tagging_table}.tag_id = #{tag_table}.id AND #{tag_list.to_sql_conditions(:operator => "LIKE")} " +
111
+ "WHERE #{tagging_table}.taggable_type = #{quote_value(base_class.name)})"
112
+
113
+ elsif options.delete(:any)
114
+ conditions << tag_list.to_sql_conditions(:operator => "LIKE")
115
+
116
+ tagging_join = " JOIN #{tagging_table}" +
117
+ " ON #{tagging_table}.taggable_id = #{table_name}.#{primary_key}" +
118
+ " AND #{tagging_table}.taggable_type = #{quote_value(base_class.name)}" +
119
+ " JOIN #{tag_table}" +
120
+ " ON #{tagging_table}.tag_id = #{tag_table}.id"
121
+
122
+
123
+ if (finder_condition = tag_type.finder_type_condition(finder_type_condition_options.merge(:sql => true))).present?
124
+ conditions << finder_condition
125
+ end
126
+
127
+ select_clause = "DISTINCT #{table_name}.*" unless !tag_type.base? and tag_types.one?
128
+
129
+ joins << tagging_join
130
+
131
+ else
132
+ tags_by_value = tag_type.scoping(finder_type_condition_options).where_any_like(tag_list).group_by(&:value)
133
+ return scoped(:conditions => "1 = 0") unless tags_by_value.length == tag_list.length # allow for chaining
134
+
135
+ # Create only one join per tag value.
136
+ tags_by_value.each do |value, tags|
137
+ tags.each do |tag|
138
+ safe_tag = value.gsub(/[^a-zA-Z0-9]/, '')
139
+ prefix = "#{safe_tag}_#{rand(1024)}"
140
+
141
+ taggings_alias = "#{undecorated_table_name}_taggings_#{prefix}"
142
+
143
+ tagging_join = "JOIN #{tagging_table} #{taggings_alias}" +
144
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
145
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
146
+ " AND #{sanitize_sql("#{taggings_alias}.tag_id" => tags.map(&:id))}"
147
+
148
+ joins << tagging_join
149
+ end
150
+ end
151
+ end
152
+
153
+ taggings_alias, tags_alias = "#{undecorated_table_name}_taggings_group", "#{undecorated_table_name}_tags_group"
154
+
155
+ if options.delete(:match_all)
156
+ joins << "LEFT OUTER JOIN #{tagging_table} #{taggings_alias}" +
157
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
158
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
159
+
160
+
161
+ group_columns = Tagtical::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
162
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tag_list.size}"
163
+ end
164
+
165
+ scoped(:select => select_clause,
166
+ :joins => joins.join(" "),
167
+ :group => group,
168
+ :conditions => conditions.join(" AND "),
169
+ :order => options[:order],
170
+ :readonly => false)
171
+ end
172
+
173
+ def is_taggable?
174
+ true
175
+ end
176
+ end
177
+
178
+ module InstanceMethods
179
+ # all column names are necessary for PostgreSQL group clause
180
+ def grouped_column_names_for(object)
181
+ self.class.grouped_column_names_for(object)
182
+ end
183
+
184
+ def is_taggable?
185
+ self.class.is_taggable?
186
+ end
187
+
188
+ def cached_tag_list_on(context)
189
+ self[tag_type(context).tag_list_name(:cached)]
190
+ end
191
+
192
+ def tag_list_cache_set_on?(context)
193
+ variable_name = tag_type(context).tag_list_ivar
194
+ !instance_variable_get(variable_name).nil?
195
+ end
196
+
197
+ def tag_list_cache_on(context)
198
+ variable_name = tag_type(context).tag_list_ivar
199
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, Tagtical::TagList.new(tags_on(context).map(&:value)))
200
+ end
201
+
202
+ def tag_list_on(context)
203
+ tag_list_cache_on(context)
204
+ end
205
+
206
+ def tag_types
207
+ @tag_types ||= self.class.tag_types.dup
208
+ end
209
+
210
+ def all_tags_list_on(context)
211
+ variable_name = tag_type(context).tag_list_ivar(:all)
212
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
213
+
214
+ instance_variable_set(variable_name, Tagtical::TagList.new(all_tags_on(context).map(&:value)).freeze)
215
+ end
216
+
217
+ ##
218
+ # Returns all tags of a given context
219
+ def all_tags_on(context, options={})
220
+ scope = tag_scope(context, options)
221
+ if Tagtical::Tag.using_postgresql?
222
+ group_columns = grouped_column_names_for(Tagtical::Tag)
223
+ scope = scope.order("max(#{Tagtical::Tagging.table_name}.created_at)").group(group_columns)
224
+ else
225
+ scope = scope.group("#{Tagtical::Tag.table_name}.#{Tagtical::Tag.primary_key}")
226
+ end
227
+ scope.all
228
+ end
229
+
230
+ ##
231
+ # Returns all tags that aren't owned.
232
+ def tags_on(context, options={})
233
+ tag_scope(context, options).where("#{Tagtical::Tagging.table_name}.tagger_id IS NULL").all
234
+ end
235
+
236
+ def set_tag_list_on(context, new_list)
237
+ variable_name = tag_type(context).tag_list_ivar
238
+ instance_variable_set(variable_name, Tagtical::TagList.from(new_list))
239
+ end
240
+
241
+ def reload(*args)
242
+ tag_types.each do |tag_type|
243
+ instance_variable_set(tag_type.tag_list_ivar, nil)
244
+ instance_variable_set(tag_type.tag_list_ivar(:all), nil)
245
+ end
246
+
247
+ super(*args)
248
+ end
249
+
250
+ def save_tags
251
+ # Do the classes from top to bottom. We want the list from "tag" to run before "sub_tag" runs.
252
+ # Otherwise, we will end up removing taggings from "sub_tag" since they aren't on "tag'.
253
+ tag_types.sort_by(&:active_record_sti_level).each do |tag_type|
254
+ next unless tag_list_cache_set_on?(tag_type)
255
+
256
+ tag_list = tag_list_cache_on(tag_type).uniq
257
+
258
+ # Find existing tags or create non-existing tags:
259
+ tag_value_lookup = tag_type.scoping { find_or_create_tags(tag_list) }
260
+ tags = tag_value_lookup.keys
261
+
262
+ current_tags = tags_on(tag_type, :only => [:current, :parents, :children])
263
+ old_tags = current_tags - tags
264
+ new_tags = tags - current_tags
265
+
266
+ unowned_taggings = taggings.where(:tagger_id => nil)
267
+
268
+ # If relevances are specified on current tags, make sure to update those
269
+ tags_requiring_relevance_update = tag_value_lookup.map { |tag, value| tag if !value.relevance.nil? }.compact & current_tags
270
+ if tags_requiring_relevance_update.present? && (update_taggings = unowned_taggings.find_all_by_tag_id(tags_requiring_relevance_update)).present?
271
+ update_taggings.each { |tagging| tagging.update_attribute(:relevance, tag_value_lookup[tagging.tag].relevance) }
272
+ end
273
+
274
+ # Find and remove old taggings:
275
+ if old_tags.present? && (old_taggings = unowned_taggings.find_all_by_tag_id(old_tags)).present?
276
+ old_taggings.reject! do |tagging|
277
+ if tagging.tag.class > tag_type.klass! # parent of current tag type/class, make sure not to remove these taggings.
278
+ update_tagging_with_inherited_tag!(tagging, new_tags, tag_value_lookup)
279
+ true
280
+ end
281
+ end
282
+ Tagtical::Tagging.destroy_all :id => old_taggings.map(&:id) # Destroy old taggings:
283
+ end
284
+
285
+ new_tags.each do |tag|
286
+ taggings.create!(:tag_id => tag.id, :taggable => self, :relevance => tag_value_lookup[tag].relevance) # Create new taggings:
287
+ end
288
+ end
289
+
290
+ true
291
+ end
292
+
293
+ private
294
+
295
+ def tag_scope(input, options={})
296
+ tags.where(tag_type(input).finder_type_condition(options))
297
+ end
298
+
299
+ # Returns the tag type for the given context and adds any new types tag_types array on this instance.
300
+ def tag_type(input)
301
+ (@tag_type ||= {})[input] ||= Tagtical::Tag::Type[input].tap do |tag_type|
302
+ tag_types << tag_type unless tag_types.include?(tag_type)
303
+ end
304
+ end
305
+
306
+ # Lets say tag class A inherits from B and B has a tag with value "foo". If we tag A with value "foo",
307
+ # we want B to have only one instance of "foo" and that tag should be an instance of A (a subclass of B).
308
+ def update_tagging_with_inherited_tag!(tagging, tags, tag_value_lookup)
309
+ if tags.present? && (tag = Tagtical::Tag.send(:detect_comparable, tags, tagging.tag.value))
310
+ tagging.update_attributes!(:tag => tag, :relevance => tag_value_lookup[tag].relevance)
311
+ tags.delete(tag)
312
+ end
313
+ end
314
+
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,110 @@
1
+ module Tagtical::Taggable
2
+ module Ownership
3
+ def self.included(base)
4
+ base.send :include, Tagtical::Taggable::Ownership::InstanceMethods
5
+ base.extend Tagtical::Taggable::Ownership::ClassMethods
6
+
7
+ base.after_save :save_owned_tags
8
+
9
+ base.initialize_acts_as_taggable_ownership
10
+ end
11
+
12
+ module ClassMethods
13
+ def acts_as_taggable(*args)
14
+ initialize_acts_as_taggable_ownership
15
+ super(*args)
16
+ end
17
+
18
+ def initialize_acts_as_taggable_ownership
19
+ tag_types.each do |tag_type|
20
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
21
+ def #{tag_type.pluralize}_from(owner)
22
+ owner_tag_list_on(owner, '#{tag_type}')
23
+ end
24
+ RUBY
25
+ end
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def owner_tags_on(owner, context, options={})
31
+ scope = tag_scope(context, options)
32
+ if owner
33
+ scope = scope.where([%{#{Tagtical::Tagging.table_name}.tagger_id = ?}, owner.id])
34
+ scope = scope.where([%{#{Tagtical::Tagging.table_name}.tagger_type = ?}, owner.class.to_s]) if Tagtical.config.polymorphic_tagger?
35
+ end
36
+ scope.all
37
+ end
38
+
39
+ def cached_owned_tag_list_on(context)
40
+ variable_name = tag_type(context).tag_list_ivar(:owned)
41
+ cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
42
+ end
43
+
44
+ def owner_tag_list_on(owner, context)
45
+ cache = cached_owned_tag_list_on(context)
46
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
47
+
48
+ cache[owner] ||= Tagtical::TagList.new(*owner_tags_on(owner, context).map(&:value))
49
+ end
50
+
51
+ def set_owner_tag_list_on(owner, context, new_list)
52
+ cache = cached_owned_tag_list_on(context)
53
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
54
+
55
+ cache[owner] = Tagtical::TagList.from(new_list)
56
+ end
57
+
58
+ def reload(*args)
59
+ tag_types.each do |tag_type|
60
+ instance_variable_set(tag_type.tag_list_ivar(:owned), nil)
61
+ end
62
+
63
+ super(*args)
64
+ end
65
+
66
+ def save_owned_tags
67
+ # Do the classes from top to bottom. We want the list from "tag" to run before "sub_tag" runs.
68
+ # Otherwise, we will end up removing taggings from "sub_tag" since they aren't on "tag'.
69
+ tag_types.sort_by(&:active_record_sti_level).each do |tag_type|
70
+ cached_owned_tag_list_on(tag_type).each do |owner, tag_list|
71
+ # Find existing tags or create non-existing tags:
72
+ tag_value_lookup = tag_type.scoping { find_or_create_tags(tag_list) }
73
+ tags = tag_value_lookup.keys
74
+
75
+ owned_tags = owner_tags_on(owner, tag_type, :only => [:current, :parents, :children])
76
+ old_tags = owned_tags - tags
77
+ new_tags = tags - owned_tags
78
+
79
+ # Find and remove old taggings:
80
+ if old_tags.present? && (old_taggings = owner_taggings(owner).find_all_by_tag_id(old_tags)).present?
81
+ old_taggings.reject! do |tagging|
82
+ if tagging.tag.class > tag_type.klass! # parent of current tag type/class, make sure not to remove these taggings.
83
+ update_tagging_with_inherited_tag!(tagging, new_tags, tag_value_lookup)
84
+ true
85
+ end
86
+ end
87
+ Tagtical::Tagging.destroy_all :id => old_taggings.map(&:id) # Destroy old taggings:
88
+ end
89
+
90
+ # Create new taggings:
91
+ new_tags.each do |tag|
92
+ taggings.create!(:tagger => owner, :relevance => tag_value_lookup[tag].relevance, :tag_id => tag.id)
93
+ end
94
+ end
95
+ end
96
+
97
+ true
98
+ end
99
+
100
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
101
+ # have the correct context, and are removed from the list.
102
+ def owner_taggings(owner)
103
+ relation = taggings.where(:tagger_id => owner.id)
104
+ relation = relation.where(:tagger_type => owner.class.to_s) if Tagtical.config.polymorphic_tagger?
105
+ relation
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,60 @@
1
+ module Tagtical::Taggable
2
+ module Related
3
+ def self.included(base)
4
+ base.send :include, Tagtical::Taggable::Related::InstanceMethods
5
+ base.extend Tagtical::Taggable::Related::ClassMethods
6
+ base.initialize_tagtical_related
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_tagtical_related
11
+ tag_types.each do |tag_type|
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ def find_related_#{tag_type.pluralize}(options = {})
14
+ related_tags_for('#{tag_type}', self.class, options)
15
+ end
16
+ alias_method :find_related_on_#{tag_type.pluralize}, :find_related_#{tag_type.pluralize}
17
+
18
+ def find_related_#{tag_type.pluralize}_for(klass, options = {})
19
+ related_tags_for('#{tag_type}', klass, options)
20
+ end
21
+
22
+ def find_matching_contexts(search_context, result_context, options = {})
23
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
24
+ end
25
+
26
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
27
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+
33
+ def acts_as_taggable(*args)
34
+ super(*args)
35
+ initialize_tagtical_related
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+ def matching_contexts_for(search_context, result_context, klass, options = {})
41
+ related_tags_for(search_context, klass, options.update(:result_context => result_context))
42
+ end
43
+
44
+ def related_tags_for(context, klass, options = {})
45
+ tags_to_find = tags_on(context).collect { |t| t.value }
46
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
47
+ group_columns = Tagtical::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
48
+
49
+ conditions = ["#{exclude_self} #{klass.table_name}.id = #{Tagtical::Tagging.table_name}.taggable_id AND #{Tagtical::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagtical::Tagging.table_name}.tag_id = #{Tagtical::Tag.table_name}.id AND #{Tagtical::Tag.table_name}.value IN (?)", tags_to_find]
50
+ conditions[0] << tag_type(options.delete(:result_context)).finder_type_condition(:sql => :append).to_s if options[:result_context]
51
+
52
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{Tagtical::Tag.table_name}.id) AS count",
53
+ :from => "#{klass.table_name}, #{Tagtical::Tag.table_name}, #{Tagtical::Tagging.table_name}",
54
+ :conditions => conditions,
55
+ :group => group_columns,
56
+ :order => "count DESC" }.update(options))
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ module Tagtical
2
+ module Taggable
3
+ def taggable?
4
+ false
5
+ end
6
+
7
+ ##
8
+ # Make a model taggable on specified contexts.
9
+ #
10
+ # @param [Array] tag_types An array of taggable contexts. These must have an associated subclass under Tag.
11
+ #
12
+ # Example:
13
+ # module Tag
14
+ # class Language < Tagtical::Tag
15
+ # end
16
+ # class Skill < Tagtical::Tag
17
+ # end
18
+ # end
19
+ # class User < ActiveRecord::Base
20
+ # acts_as_taggable :languages, :skills
21
+ # end
22
+ def acts_as_taggable(*tag_types)
23
+ tag_types.flatten!
24
+ tag_types << Tagtical::Tag::Type::BASE # always include the base type.
25
+ tag_types = Tagtical::Tag::Type[tag_types.uniq.compact]
26
+
27
+ if taggable?
28
+ write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
29
+ else
30
+ write_inheritable_attribute(:tag_types, tag_types)
31
+ class_inheritable_reader(:tag_types)
32
+
33
+ class_eval do
34
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "Tagtical::Tagging"
35
+ has_many :tags, :through => :taggings, :class_name => "Tagtical::Tag"
36
+
37
+ def self.taggable?
38
+ true
39
+ end
40
+
41
+ include Tagtical::Taggable::Core
42
+ include Tagtical::Taggable::Collection
43
+ include Tagtical::Taggable::Cache
44
+ include Tagtical::Taggable::Ownership
45
+ include Tagtical::Taggable::Related
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ module Tagtical
2
+ class Tagging < ::ActiveRecord::Base #:nodoc:
3
+ include Tagtical::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
+
5
+ attr_accessible :tag,
6
+ :tag_id,
7
+ :taggable,
8
+ :taggable_type,
9
+ :taggable_id,
10
+ :tagger,
11
+ :tagger_id,
12
+ :relevance
13
+
14
+ belongs_to :tag, :class_name => 'Tagtical::Tag'
15
+ belongs_to :taggable, :polymorphic => true
16
+
17
+ validates_presence_of :tag_id
18
+ validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :tagger_id]
19
+
20
+ if Tagtical.config.polymorphic_tagger?
21
+ attr_accessible :tagger_type
22
+ belongs_to :tagger, :polymorphic => true
23
+ else
24
+ belongs_to :tagger, case Tagtical.config.tagger
25
+ when Hash then Tagtical.config.tagger
26
+ when true then {:class_name => "User"} # default to using User class.
27
+ when String then {:class_name => Tagtical.config.tagger}
28
+ end
29
+ end
30
+
31
+
32
+ before_create { |record| record.relevance ||= default_relevance }
33
+
34
+ class_attribute :default_relevance, :instance_writer => false
35
+ self.default_relevance = 1
36
+
37
+ def <=>(tagging)
38
+ relevance <=> tagging.relevance
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module Tagtical
2
+ module TagsHelper
3
+ # See the README for an example using tag_cloud.
4
+ def tag_cloud(tags, classes)
5
+ tags = tags.all if tags.respond_to?(:all)
6
+
7
+ return [] if tags.empty?
8
+
9
+ max_count = tags.sort_by(&:count).last.count.to_f
10
+
11
+ tags.each do |tag|
12
+ index = ((tag.count / max_count) * (classes.size - 1)).round
13
+ yield tag, classes[index]
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/tagtical.rb ADDED
@@ -0,0 +1,47 @@
1
+ require "active_record"
2
+ require "action_view"
3
+ require "active_support/hash_with_indifferent_access"
4
+
5
+ module Tagtical
6
+
7
+ # Place a tagtical.yml file in the config directory to control settings
8
+ mattr_accessor :config
9
+ self.config = ActiveSupport::InheritableOptions.new(ActiveSupport::HashWithIndifferentAccess.new.tap do |hash|
10
+ require 'yaml'
11
+ path = Rails.root.join("config", "tagtical.yml") rescue ""
12
+ hash.update(YAML.load_file(path)) if File.exists?(path)
13
+ # If tagger association options were not provided, then use the polymorphic_tagger association.
14
+ hash.reverse_merge!(
15
+ :polymorphic_tagger? => !hash[:tagger]
16
+ )
17
+ end)
18
+
19
+ end
20
+
21
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
22
+
23
+ require "tagtical/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
24
+
25
+ require "tagtical/taggable"
26
+ require "tagtical/taggable/core"
27
+ require "tagtical/taggable/collection"
28
+ require "tagtical/taggable/cache"
29
+ require "tagtical/taggable/ownership"
30
+ require "tagtical/taggable/related"
31
+
32
+ require "tagtical/acts_as_tagger"
33
+ require "tagtical/tag"
34
+ require "tagtical/tag_list"
35
+ require "tagtical/tags_helper"
36
+ require "tagtical/tagging"
37
+
38
+ $LOAD_PATH.shift
39
+
40
+ if defined?(ActiveRecord::Base)
41
+ ActiveRecord::Base.extend Tagtical::Taggable
42
+ ActiveRecord::Base.send :include, Tagtical::Tagger
43
+ end
44
+
45
+ if defined?(ActionView::Base)
46
+ ActionView::Base.send :include, Tagtical::TagsHelper
47
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'tagtical'