acts_as_word_cloud 0.0.1

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