acts_as_word_cloud 0.0.1

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 (68) hide show
  1. data/.DS_Store +0 -0
  2. data/.document +5 -0
  3. data/.gitignore_new +7 -0
  4. data/.rspec +1 -0
  5. data/CHANGELOG +3 -0
  6. data/Gemfile +22 -0
  7. data/Gemfile.lock +130 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.rdoc +122 -0
  10. data/Rakefile +52 -0
  11. data/VERSION +1 -0
  12. data/acts_as_word_cloud.gemspec +127 -0
  13. data/features/acts_as_word_cloud.feature +9 -0
  14. data/features/step_definitions/acts_as_word_cloud_steps.rb +0 -0
  15. data/features/support/env.rb +13 -0
  16. data/lib/acts_as_word_cloud/config.rb +31 -0
  17. data/lib/acts_as_word_cloud/engine.rb +4 -0
  18. data/lib/acts_as_word_cloud/railtie.rb +12 -0
  19. data/lib/acts_as_word_cloud/word_cloud.rb +227 -0
  20. data/lib/acts_as_word_cloud.rb +2 -0
  21. data/lib/generators/acts_as_word_cloud/install_generator.rb +23 -0
  22. data/lib/generators/acts_as_word_cloud/templates/config.rb +7 -0
  23. data/lib/model_methods_helper.rb +87 -0
  24. data/spec/acts_as_word_cloud_spec.rb +109 -0
  25. data/spec/dummy/README.rdoc +261 -0
  26. data/spec/dummy/Rakefile +7 -0
  27. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  28. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  30. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  31. data/spec/dummy/app/mailers/.gitkeep +0 -0
  32. data/spec/dummy/app/models/.gitkeep +0 -0
  33. data/spec/dummy/app/models/article.rb +9 -0
  34. data/spec/dummy/app/models/author.rb +7 -0
  35. data/spec/dummy/app/models/following.rb +5 -0
  36. data/spec/dummy/app/models/publisher.rb +7 -0
  37. data/spec/dummy/app/models/reader.rb +7 -0
  38. data/spec/dummy/app/models/site.rb +8 -0
  39. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/spec/dummy/config/application.rb +65 -0
  41. data/spec/dummy/config/boot.rb +10 -0
  42. data/spec/dummy/config/database.yml +25 -0
  43. data/spec/dummy/config/environment.rb +5 -0
  44. data/spec/dummy/config/environments/development.rb +37 -0
  45. data/spec/dummy/config/environments/production.rb +67 -0
  46. data/spec/dummy/config/environments/test.rb +37 -0
  47. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  48. data/spec/dummy/config/initializers/inflections.rb +15 -0
  49. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  50. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  51. data/spec/dummy/config/initializers/session_store.rb +8 -0
  52. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  53. data/spec/dummy/config/locales/en.yml +5 -0
  54. data/spec/dummy/config/routes.rb +58 -0
  55. data/spec/dummy/config.ru +4 -0
  56. data/spec/dummy/db/migrate/20121107162154_create_system.rb +64 -0
  57. data/spec/dummy/db/schema.rb +81 -0
  58. data/spec/dummy/features/support/blueprints.rb +57 -0
  59. data/spec/dummy/lib/assets/.gitkeep +0 -0
  60. data/spec/dummy/lib/model_methods_helper.rb +87 -0
  61. data/spec/dummy/log/.gitkeep +0 -0
  62. data/spec/dummy/public/404.html +26 -0
  63. data/spec/dummy/public/422.html +26 -0
  64. data/spec/dummy/public/500.html +25 -0
  65. data/spec/dummy/public/favicon.ico +0 -0
  66. data/spec/dummy/script/rails +6 -0
  67. data/spec/spec_helper.rb +21 -0
  68. metadata +211 -0
@@ -0,0 +1,227 @@
1
+ module ActsAsWordCloud
2
+ module WordCloud
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # args:
9
+ # using, takes an array of Symbols that refer to the methods we'll use on the model calling word_cloud
10
+ # excluded models, takes in Class names of models we want to ignore that are associated to model calling word_cloud
11
+ # skipped attributes, takes in Symbols referring to attributes that won't be pulled from model calling word_cloud
12
+ # depth level, takes in an Integer referring to how deep of a recursive search we'll make
13
+ # no_mixin_fields, is a default pre-set array of methods to use on models that do not include the mixin
14
+ #
15
+ def acts_as_word_cloud(args)
16
+
17
+ # getter/setter methods for the module
18
+ mattr_accessor :word_cloud_using unless respond_to? :word_cloud_using
19
+ mattr_accessor :word_cloud_excluded unless respond_to? :word_cloud_excluded
20
+ mattr_accessor :word_cloud_skipped unless respond_to? :word_cloud_skipped
21
+ mattr_accessor :word_cloud_depth unless respond_to? :word_cloud_depth
22
+ mattr_accessor :word_cloud_no_mixin_fields unless respond_to? :word_cloud_no_mixin_fields
23
+
24
+ # set empty arrays to allow |= on values
25
+ unless self.word_cloud_using.is_a?(Array)
26
+ self.word_cloud_using = []
27
+ end
28
+ unless self.word_cloud_excluded.is_a?(Array)
29
+ self.word_cloud_excluded = []
30
+ end
31
+ unless self.word_cloud_skipped.is_a?(Array)
32
+ self.word_cloud_skipped = []
33
+ end
34
+ unless self.word_cloud_no_mixin_fields.is_a?(Array)
35
+ self.word_cloud_no_mixin_fields = []
36
+ end
37
+
38
+ # default values if none set in the mixin call on the model
39
+ self.word_cloud_using |= args[:methods_to_use].present? ? args[:methods_to_use] : []
40
+ self.word_cloud_excluded |= args[:excluded_models].present? ? args[:excluded_models] : []
41
+ self.word_cloud_skipped |= args[:skipped_attributes].present? ? args[:skipped_attributes] : []
42
+ self.word_cloud_depth = args[:depth].present? ? args[:depth] : ActsAsWordCloud.config.min_depth
43
+ self.word_cloud_no_mixin_fields = [:name, :title, :label] #ActsAsWordCloud.config.no_mixin_fields
44
+
45
+ include ActsAsWordCloud::WordCloud::InstanceMethods
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+
51
+ # collects associations on a model as long as they're not nil
52
+ #
53
+ # @param [Symbol] the association type to look in
54
+ # @returns [Array <Symbol>] association names under the passed in type
55
+ #
56
+ def word_cloud_get_associated(type)
57
+ self.class.reflect_on_all_associations(type).select {|r| self.send(r.name).present? }.collect { |r| r.name }
58
+ end
59
+
60
+ # goes through array of objects or arrays (in the case of has_many association)
61
+ #
62
+ # @param [Symbol] the association type to fetch objects from
63
+ # @returns [Array <Object>] that under association passed in
64
+ #
65
+ def word_cloud_associated_objects(type)
66
+ objects = []
67
+ associated = word_cloud_get_associated(type)
68
+ associated.each do |o|
69
+ if o.class == Array
70
+ nested_array = self.send(o)
71
+ nested_array.each do |a|
72
+ objects << a
73
+ end
74
+ else
75
+ objects << self.send(o)
76
+ end
77
+ end
78
+ return objects
79
+ end
80
+
81
+ # removes objects that are in the list of objects to exclude
82
+ #
83
+ # @returns [Array <Object>] that are not in the excluded list
84
+ #
85
+ def word_cloud_exclude_words(objects)
86
+ if objects.nil?
87
+ return []
88
+ else
89
+ result = objects.flatten.reject { |x| self.word_cloud_excluded.include?(x.class) }
90
+ return result.present? ? result : []
91
+ end
92
+ end
93
+
94
+ # removes objects that include word_cloud mixin
95
+ #
96
+ # @returns [Array <Object>] that don't include the mixin
97
+ #
98
+ def word_cloud_no_mixin(objects)
99
+ if objects.nil?
100
+ return []
101
+ else
102
+ result = objects.flatten.reject { |n| n.respond_to?(:word_cloud) }
103
+ result.present? ? result : []
104
+ end
105
+ end
106
+
107
+ # goes through each object passed in trying the included methods for each of those objects
108
+ # keeps the first one to work and returns the value of that method called on the object
109
+ #
110
+ # @param [Array <Objects>] to look through for values
111
+ # @returns [Array <String>] values returned by method
112
+ #
113
+ def word_cloud_process_words(objects)
114
+ output = []
115
+ objects.each do |obj|
116
+ obj_calls = obj.word_cloud_using.select { |f| obj.respond_to?(f) }
117
+ if obj_calls.first.present?
118
+ output << obj.send(obj_calls.first)
119
+ end
120
+ end
121
+ return output
122
+ end
123
+
124
+ # goes through each of the default fields to call on models that don't include mixin
125
+ # and attempts to return the value of the first one to work, if none do return model class name
126
+ #
127
+ # @returns [String] value for object passed in
128
+ #
129
+ def word_cloud_apply_fields_to(no_mixin_model)
130
+ # fields that should be tried on models that don't have the mixin, set with other attributes
131
+ fields = self.word_cloud_no_mixin_fields
132
+ fields.each do |f|
133
+ if no_mixin_model.respond_to?(f)
134
+ return no_mixin_model.send(f)
135
+ end
136
+ end
137
+ return no_mixin_model.class.name
138
+ end
139
+
140
+ # goes through models that don't include mixin trying to find a relevant value for each
141
+ #
142
+ # @returns [Array <String>] of values for objects passed in
143
+ #
144
+ def word_cloud_find_field(no_mixin)
145
+ output = []
146
+ flag = 0
147
+ no_mixin.each do |n|
148
+ output << word_cloud_apply_fields_to(n)
149
+ end
150
+ return output
151
+ end
152
+
153
+
154
+ # returns values from methods specified in using option
155
+ # ignores string attributes that are listed in the skipped option
156
+ #
157
+ # @returns [Array <String>]
158
+ #
159
+ def word_cloud_get_valid_strings
160
+ output = []
161
+ ignore = []
162
+
163
+ self.word_cloud_using.each do |m|
164
+ output << self.send(m)
165
+ end
166
+
167
+ self.word_cloud_skipped.each do |s|
168
+ ignore << self.send(s)
169
+ end
170
+
171
+ # current model's string attributes
172
+ output += self.attributes.select {|k,v| v.class == String}.values
173
+ output -= ignore
174
+
175
+ return output
176
+ end
177
+
178
+ # finds string attributes on model
179
+ # processes associations on model, either fetching word_cloud results if they include mixin or default information if they don't
180
+ # if depth is said to something higher than one the word_cloud results on each associated model then makes more recursive calls (BFS)
181
+ #
182
+ # @params Integer, recursive depth for method; Symbol for the type of result (array or string)
183
+ # @returns [Array <String>] all processed values
184
+ #
185
+ def word_cloud( depth = self.word_cloud_depth, type = :string )
186
+
187
+ output = []
188
+ objects = []
189
+ no_mixin = []
190
+
191
+ # string attributes in current model
192
+ output = word_cloud_get_valid_strings
193
+
194
+ # objects on all associations collected
195
+ objects += self.word_cloud_associated_objects(:belongs_to)
196
+ objects += self.word_cloud_associated_objects(:has_one)
197
+ objects += self.word_cloud_associated_objects(:has_many)
198
+
199
+ # remove objects that have been explicitly excluded on current model
200
+ objects = word_cloud_exclude_words(objects)
201
+ # find associated models that do not include mixin
202
+ no_mixin = word_cloud_no_mixin(objects)
203
+ objects -= no_mixin
204
+
205
+ # process object using set methods for each model with mixin, or try preset general methods for models without
206
+ output += self.word_cloud_process_words(objects) + word_cloud_find_field(no_mixin)
207
+
208
+ # recursive step, if depth > 1, calls word cloud on objects array (which already excludes objects that don't have the mixin)
209
+ if depth < 1
210
+ return "depth for word cloud can't be less than 0"
211
+ elsif depth == 1
212
+ # recursive steps got a bit more complicated with :array/:string addition, that last line gets rid of duplicate results in the 'long' strings being returned
213
+ return ( type == :array ? output.uniq : output.uniq.join(' ') )
214
+ else
215
+ if type == :array
216
+ output |= objects.collect { |o| o.word_cloud(depth-1, :array) }
217
+ return output.flatten.uniq
218
+ else
219
+ output |= objects.collect { |o| o.word_cloud(depth-1) }
220
+ return output.flatten.join(' ').split(' ').uniq.join(' ')
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+
@@ -0,0 +1,2 @@
1
+ require 'acts_as_word_cloud/railtie'
2
+ require 'acts_as_word_cloud/engine'
@@ -0,0 +1,23 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+ require 'active_record'
4
+ require 'rails/generators/active_record'
5
+
6
+ module ActsAsWordCloud
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path(File.join(File.dirname(__FILE__), "templates"))
12
+
13
+ desc <<DESC
14
+ Description:
15
+ Copies over migrations and config for the normalizer.
16
+ DESC
17
+
18
+ def copy_config_file
19
+ copy_file "config.rb", "config/initializers/acts_as_word_cloud.rb"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ ActsAsWordCloud.configure do |config|
2
+ #the default setting for smallest recursive step for calling wordcloud on models
3
+ config.min_depth = 1
4
+
5
+ #methods whose values we'll attempt to get for models that don't include the mixin
6
+ config.no_mixin_fields = [:name, :title, :label]
7
+ end
@@ -0,0 +1,87 @@
1
+ require 'find'
2
+ class ModelMethodsHelper
3
+
4
+ # finds or makes a model based on the given attributes
5
+ # @param [Constant] model_name The name of the model
6
+ # @param [Hash] attributes The attributes to find the model or add to the new one
7
+ def self.find_or_make_by_attributes(model_name, attributes)
8
+ instances = model_name.where(attributes)
9
+ if instances.empty?
10
+ return model_name.make(attributes)
11
+ else
12
+ return instances.first
13
+ end
14
+ end
15
+
16
+ # Uses the descendants_of method to determine the model structure
17
+ # Then tableizes and converts to symbol
18
+ #
19
+ # Load all models must be run for accurate descendants list
20
+ # it is now being called every time this method is called
21
+ # for now it works because this is only called one time when the authorization config is loaded on initialize
22
+ # if this ever changes, this code may need to be update (2-15-2012)
23
+ #
24
+ # @return [Array] Array with file names underscored and pluralized in the format [:users, :case_placements, :roles, :job_locations, ... ]
25
+ def self.get_model_symbol_array
26
+ # Old method:
27
+ # Major redesign of this method that finds namespaced models
28
+ # Finds all files in the given folder, tries to find the ruby files
29
+ # Then processes the names to get the names we need for the code
30
+ #
31
+ # @return [Array] Array with file names underscored and pluralized in the format [:users, :case_placements, :roles, :job_locations, ... ]
32
+ #model_array = []
33
+ #Find.find("#{Rails.root.to_s}/app/models") do |model_path|
34
+ # if model_path.match(/\.rb$/)
35
+ # model_path = model_path.gsub("#{Rails.root.to_s}/app/models/", "")
36
+ # model_path = model_path.gsub("/","_")
37
+ # model_path = model_path.gsub(/\.rb$/, "")
38
+ # model_array.push model_path.pluralize.to_sym
39
+ # end
40
+ #end
41
+ #return model_array
42
+
43
+ # new method
44
+ self.load_all_models
45
+ # returns the table name if there is one or tableizes the model name if not
46
+ # our permissions system generally uses table names for model permissions
47
+ return self.descendants_of(ActiveRecord::Base).map{ |m| m.abstract_class ? m.to_s.tableize.to_sym : m.table_name.to_sym }
48
+ end
49
+
50
+ # Requires all models so that they are visible in the object space
51
+ # This is only necessary when we are not caching classes
52
+ def self.load_all_models
53
+ if !::Rails.application.config.cache_classes
54
+ Dir[Rails.root.join("app/models/**/*.rb")].each {|f| require f}
55
+ end
56
+ end
57
+
58
+ # Return a list of descendants of the given model.
59
+ #
60
+ # If direct is set to true, only return immediate descendants
61
+ # The load_all_models must be called before this is called or the result may not be correct
62
+ #
63
+ # @param [Constant] parent_class The parent class to look for descendants for
64
+ # @param [Boolean] direct Whether to look for all descendants or immediate descendants
65
+ # @return [Array] An array of class constants that satisfy the conditions of the parameters
66
+ def self.descendants_of(parent_class, direct = false)
67
+ classes = []
68
+ ObjectSpace.each_object(::Class).each do |klass|
69
+ if direct
70
+ classes << klass if klass.superclass == parent_class
71
+ else
72
+ classes << klass if klass < parent_class
73
+ end
74
+ end
75
+ return classes
76
+ end
77
+
78
+ # Convenience method to return the results of descendants_of with direct set to true
79
+ #
80
+ # The load_all_models must be called before this is called or the result may not be correct
81
+ #
82
+ # @param [Constant] parent_class The parent class to look for descendants for
83
+ # @return [Array] An array of class constants that satisfy the conditions of the parameters
84
+ def self.direct_descendants_of(parent_class)
85
+ return self.descendants_of(parent_class, true)
86
+ end
87
+ end
@@ -0,0 +1,109 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "ActsAsWordCloud" do
4
+ before(:all) do
5
+ @lone_article = Article.make
6
+ @publisher = Publisher.make(:name => "publisher", :location => "location")
7
+ @site = Site.make( :name => "site name", :domain => "uff.us", :genre => "site genre", :publisher => @publisher)
8
+ @author = Author.make(:name => "author", :genre => "author genre", :publisher => @publisher, :site => @site)
9
+ @article = Article.make(:title => "article title", :genre => "genre", :content => "article text", :author => @author, :publisher => @publisher, :site => @site)
10
+ @reader = Reader.make(:username => "reader", :site => @site)
11
+ @following = Following.make(:article => @article, :reader => @reader, :special_name => "following1")
12
+ end
13
+
14
+ describe "word_cloud" do
15
+ describe "class method use_word_cloud" do
16
+ it "should contain set or default values in attributes" do
17
+ @article.word_cloud_using.should == [:title]
18
+ @article.word_cloud_excluded.should == []
19
+ @article.word_cloud_depth.should == 1
20
+ @article.word_cloud_no_mixin_fields.should == [:name, :title, :label]
21
+ end
22
+ end
23
+
24
+ it "should return the right association models" do
25
+ @lone_article.word_cloud.should == [@lone_article.title, @lone_article.genre, @lone_article.content, @lone_article.author.name, @lone_article.publisher.name, @lone_article.site.domain ].join(' ')
26
+ @article.word_cloud.should == [@article.title, @article.genre, @article.content, @article.author.name, @article.publisher.name, @article.site.domain, @article.readers.first.username, "Following" ].join(' ')
27
+
28
+ @publisher2 = Publisher.make(:name => "publisher2", :location => "location2")
29
+ @site2 = Site.make( :name => "site2 name", :domain => "uff.us2", :genre => "site2 genre", :publisher => @publisher2)
30
+ @author2 = Author.make(:name => "author2", :genre => "author2 genre", :publisher => @publisher2, :site => @site2)
31
+ @article2 = Article.make(:title => "article2 title", :genre => "genre2", :content => "article2 text", :author => @author2, :publisher => @publisher2, :site => @site2)
32
+ @following2 = Following.make(:article => @article2, :reader => @reader , :special_name => "following2")
33
+
34
+ @reader.word_cloud(2, :array).should == [@reader.username, @reader.site.domain, @article.title, @article2.title, "Following", @site.name, @site.genre, @site.publisher.name, @site.authors.first.name, @article.genre, @article.content, @article2.genre, @article2.content, @article2.author.name, @article2.publisher.name, @article2.site.domain ]
35
+ end
36
+ end
37
+
38
+ describe "instance methods:" do
39
+ describe "word_cloud_associated_objects" do
40
+ it "should return correct models when called with belongs_to" do
41
+ @article.word_cloud_associated_objects(:belongs_to).should == [@article.author, @article.publisher, @article.site]
42
+ end
43
+
44
+ it "should return correct models when called with has_many" do
45
+ @article.word_cloud_associated_objects(:has_many).should == [@article.followings, @article.readers]
46
+ end
47
+ end
48
+
49
+ describe "word_cloud_get_associated" do
50
+ it "should return collectoin of non-nil associated models" do
51
+ @article.word_cloud_get_associated(:belongs_to).should == [:author, :publisher, :site]
52
+ @lone_article.word_cloud_get_associated(:has_many).should == []
53
+ @article.word_cloud_get_associated(:has_many).should == [:followings, :readers]
54
+ end
55
+ end
56
+
57
+ describe "word_cloud_exclude_words" do
58
+ it "should not keep excluded models" do
59
+ @article.stub!(:word_cloud_excluded).and_return([Publisher])
60
+ objects = @article.word_cloud_associated_objects(:belongs_to)
61
+ @article.word_cloud_exclude_words(objects).should == [@article.author, @article.site]
62
+ end
63
+ end
64
+
65
+ describe "word_cloud_no_mixin" do
66
+ it "should only have models that don't have the mixin" do
67
+ objects = @article.word_cloud_associated_objects(:has_many)
68
+ @article.word_cloud_no_mixin(objects).should == @article.followings
69
+ end
70
+ end
71
+
72
+ describe "word_cloud_process_words" do
73
+ it "should return result of first included method to work on objects with mixin" do
74
+ objects = []
75
+ objects += @article.word_cloud_associated_objects(:belongs_to)
76
+ objects += @article.word_cloud_associated_objects(:has_many)
77
+ objects = @article.word_cloud_exclude_words(objects)
78
+ no_mixin = @article.word_cloud_no_mixin(objects)
79
+ objects -= no_mixin
80
+ @article.word_cloud_process_words(objects).should == [@article.author.name, @article.publisher.name, @article.site.domain, @article.readers.first.username]
81
+ end
82
+ end
83
+
84
+ describe "word_cloud_apply_fields_to" do
85
+ it "return the value found in the first method to work on object passed in, if there's none return class name" do
86
+ @article.word_cloud_apply_fields_to(@article.followings.first).should == "Following"
87
+ @article.stub!(:word_cloud_no_mixin_fields).and_return([:special_name])
88
+ @article.word_cloud_apply_fields_to(@article.followings.first).should == @article.followings.first.special_name
89
+ end
90
+ end
91
+
92
+ describe "word_cloud_find_field" do
93
+ it "should return result of first included method to work on objects with mixin or their class names" do
94
+ @article.word_cloud_find_field(@article.followings).should == ["Following"]
95
+ end
96
+ end
97
+
98
+ describe "word_cloud_get_valid_strings" do
99
+ it "should return used methods on model and string attributes not in skipped list" do
100
+ @article.stub!(:word_cloud_skipped).and_return([:title, :content])
101
+ @article.word_cloud_get_valid_strings.should_not include "article title"
102
+ @article.word_cloud_get_valid_strings.should_not include "article text"
103
+ @article.word_cloud_get_valid_strings.should include "genre"
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+