completeness-fu 0.5.0

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.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 5
4
+ :patch: 0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'completeness-fu'
@@ -0,0 +1,20 @@
1
+ require 'completeness-fu/active_record_additions'
2
+ require 'completeness-fu/scoring_builder'
3
+
4
+ module ActiveRecord
5
+ Base.class_eval do
6
+ include CompletenessFu::ActiveRecordAdditions
7
+ end
8
+ end
9
+
10
+
11
+ CompletenessFu.common_weightings = { :low => 20, :medium => 40, :high => 60 }
12
+
13
+ CompletenessFu.default_weightings = 40
14
+
15
+ CompletenessFu.default_i18n_namespace = [:completeness_scoring, :models]
16
+
17
+ CompletenessFu.default_grading = { :poor => 0..24,
18
+ :low => 25..49,
19
+ :medium => 50..79,
20
+ :high => 80..100 }
@@ -0,0 +1,112 @@
1
+ module CompletenessFu
2
+
3
+ class << self
4
+ attr_accessor :common_weightings
5
+ attr_accessor :default_weightings
6
+ attr_accessor :default_i18n_namespace
7
+ attr_accessor :default_grading
8
+ end
9
+
10
+
11
+ module ActiveRecordAdditions
12
+
13
+ def self.included(base)
14
+ base.class_eval do
15
+ def self.define_completeness_scoring(&checks_block)
16
+ class_inheritable_array :completeness_checks
17
+ cattr_accessor :default_weighting
18
+ cattr_accessor :model_weightings
19
+
20
+ self.send :extend, ClassMethods
21
+ self.send :include, InstanceMethods
22
+
23
+ checks_results = CompletenessFu::ScoringBuilder.generate(self, &checks_block)
24
+
25
+ self.default_weighting = checks_results[:default_weighting]
26
+ self.completeness_checks = checks_results[:completeness_checks]
27
+ self.model_weightings = checks_results[:model_weightings]
28
+ self.before_validation checks_results[:cache_score_details] if checks_results[:cache_score_details]
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+ module ClassMethods
35
+ def max_completeness_score
36
+ self.completeness_checks.inject(0) { |score, check| score += check[:weighting] }
37
+ end
38
+ end
39
+
40
+
41
+ module InstanceMethods
42
+ # returns an array of hashes with the translated name, description + weighting
43
+ def failed_checks
44
+ self.completeness_checks.inject([]) do |failures, check|
45
+ failures << translate_check_details(check) if not check[:check].call(self)
46
+ failures
47
+ end
48
+ end
49
+
50
+ # returns an array of hashes with the translated name, description + weighting
51
+ def passed_checks
52
+ self.completeness_checks.inject([]) do |passed, check|
53
+ case check[:check]
54
+ when Proc
55
+ passed << translate_check_details(check) if check[:check].call(self)
56
+ when Symbol
57
+ passed << translate_check_details(check) if self.send check[:check]
58
+ end
59
+
60
+ passed
61
+ end
62
+ end
63
+
64
+ # returns the absolute complete score
65
+ def completeness_score
66
+ sum_score = 0
67
+ passed_checks.each { |check| sum_score += check[:weighting] }
68
+ sum_score
69
+ end
70
+
71
+ # returns the percentage of completeness (relative score)
72
+ def percent_complete
73
+ self.completeness_score.to_f / self.class.max_completeness_score.to_f * 100
74
+ end
75
+
76
+ # returns a basic 'grading' based on percent_complete, defaults are :high, :medium, :low, and :poor
77
+ def completeness_grade
78
+ CompletenessFu.default_grading.each do |grading|
79
+ return grading.first if grading.last.include?(self.percent_complete)
80
+ end
81
+ end
82
+
83
+
84
+ private
85
+
86
+ def translate_check_details(full_check)
87
+ namespace = CompletenessFu.default_i18n_namespace + [self.class.name.downcase.to_sym, full_check[:name]]
88
+
89
+ translations = [:title, :description, :extra].inject({}) do |list, field|
90
+ list[field] = I18n.t(field.to_sym, :scope => namespace)
91
+ list
92
+ end
93
+
94
+ full_check.merge(translations)
95
+ end
96
+
97
+ def cache_completeness_score(score_type)
98
+ score = case score_type
99
+ when :relative
100
+ self.percent_complete
101
+ when :absolute
102
+ self.completeness_score
103
+ else
104
+ raise ArgumentException, 'completeness scoring type not recognized'
105
+ end
106
+ self.cached_completeness_score = score.round
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,46 @@
1
+ module CompletenessFu
2
+
3
+ # A simple clean room for setting up the completeness check information
4
+ class ScoringBuilder
5
+
6
+ attr_accessor :completeness_checks, :model_weightings, :cache_score_details, :default_weighting
7
+
8
+ def self.generate(model, &block)
9
+ sb = ScoringBuilder.new
10
+
11
+ sb.completeness_checks = []
12
+ sb.default_weighting = CompletenessFu.default_weightings
13
+ sb.model_weightings = CompletenessFu.common_weightings
14
+
15
+ sb.instance_eval(&block)
16
+
17
+ { :completeness_checks => sb.completeness_checks,
18
+ :model_weightings => sb.model_weightings,
19
+ :cache_score_details => sb.cache_score_details,
20
+ :default_weighting => sb.default_weighting }
21
+ end
22
+
23
+
24
+ private
25
+
26
+ def check(name, check, weighting = nil)
27
+ weighting ||= self.default_weighting
28
+ weighting = self.model_weightings[weighting] if weighting.is_a?(Symbol)
29
+ self.completeness_checks << { :name => name, :check => check, :weighting => weighting}
30
+ end
31
+
32
+ def weightings(custom_weighting_opts)
33
+ use_common = custom_weighting_opts.delete(:merge_with_common)
34
+ if use_common
35
+ self.model_weightings.merge!(custom_weights)
36
+ else
37
+ self.model_weightings = custom_weighting_opts
38
+ end
39
+ end
40
+
41
+ def cache_score(score_type = :relative)
42
+ self.cache_score_details = lambda { |instance| instance.send :cache_completeness_score, score_type }
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1 @@
1
+ # Logfile created on Wed Sep 23 14:40:19 +0200 2009 by logger.rb/22285
@@ -0,0 +1,8 @@
1
+ en:
2
+ completeness_scoring:
3
+ models:
4
+ scoringtest:
5
+ title:
6
+ title: 'Title'
7
+ description: 'The Scoring Test Description'
8
+ extra: 'Just give it a title'
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ require 'active_record'
7
+ require 'action_controller'
8
+
9
+ begin
10
+ require 'ruby-debug'
11
+ rescue LoadError
12
+ puts "ruby-debug not loaded"
13
+ end
14
+
15
+ ROOT = File.join(File.dirname(__FILE__), '..')
16
+
17
+ $LOAD_PATH << File.join(ROOT, 'lib')
18
+ $LOAD_PATH << File.join(ROOT, 'lib', 'completeness-fu')
19
+
20
+ require File.join(ROOT, 'lib', 'completeness-fu.rb')
21
+
22
+
23
+ TEST_DATABASE_FILE = File.join(ROOT, 'test', 'test.sqlite3')
24
+
25
+ File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
26
+ ActiveRecord::Base.establish_connection(
27
+ "adapter" => "sqlite3", "database" => TEST_DATABASE_FILE
28
+ )
29
+
30
+ RAILS_DEFAULT_LOGGER = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
31
+
32
+ load(File.dirname(__FILE__) + '/schema.rb')
33
+
34
+
35
+ I18n.load_path << File.join(ROOT, 'test', 'en.yml')
36
+
37
+
38
+ def rebuild_class options = {}
39
+ ActiveRecord::Base.send(:include, CompletenessFu::ActiveRecordAdditions)
40
+ Object.send(:remove_const, "ScoringTest") rescue nil
41
+ Object.const_set("ScoringTest", Class.new(ActiveRecord::Base))
42
+ ScoringTest.class_eval do
43
+ include CompletenessFu::ActiveRecordAdditions
44
+ define_completeness_scoring do
45
+ check :title, lambda { |test| test.title.present? }, 20
46
+ end
47
+ end
48
+ end
49
+
50
+ def reset_class class_name
51
+ ActiveRecord::Base.send(:include, CompletenessFu::ActiveRecordAdditions)
52
+ Object.send(:remove_const, class_name) rescue nil
53
+ klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
54
+ klass.class_eval{ include CompletenessFu::ActiveRecordAdditions }
55
+ klass
56
+ end
@@ -0,0 +1,6 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+ create_table "scoring_tests", :force => true do |t|
3
+ t.string :title
4
+ t.string :cached_completeness_score
5
+ end
6
+ end
@@ -0,0 +1,198 @@
1
+ # encoding: utf-8
2
+ require 'test/helper'
3
+
4
+
5
+ class ScoringTest < Test::Unit::TestCase
6
+
7
+ context "An ActiveRecord child class" do
8
+ should "have a define_completeness_scoring mixed in" do
9
+ reset_class 'ScoringTest'
10
+ assert ScoringTest.methods.include?('define_completeness_scoring')
11
+ end
12
+ end
13
+
14
+
15
+ context "A class with scoring defined" do
16
+ setup do
17
+ reset_class 'ScoringTest'
18
+ ScoringTest.class_eval do
19
+ define_completeness_scoring do
20
+ check :title, lambda { |test| test.title.present? }, 20
21
+ end
22
+ end
23
+ end
24
+
25
+ should "have one scoring check" do
26
+ assert_equal 1, ScoringTest.completeness_checks.size
27
+ end
28
+
29
+ should "have one failed check" do
30
+ st = ScoringTest.new
31
+ assert_equal 1, st.failed_checks.size
32
+ end
33
+
34
+
35
+ context "and with one complete check" do
36
+ setup do
37
+ @st = ScoringTest.new
38
+ @st.title = 'I have a title'
39
+ end
40
+
41
+ should "have an absolute completeness score of 20" do
42
+ assert_equal 20, @st.completeness_score
43
+ end
44
+
45
+ should "have a relative completeness score of 0 (percent complete)" do
46
+ assert_equal 100, @st.percent_complete
47
+ end
48
+
49
+ should "have a description" do
50
+ assert_equal "The Scoring Test Description", @st.passed_checks.first[:description]
51
+ end
52
+ end
53
+ end
54
+
55
+
56
+ context "A class with scoring defined with no weighting" do
57
+ setup do
58
+ reset_class 'ScoringTest'
59
+ ScoringTest.class_eval do
60
+ define_completeness_scoring do
61
+ check :title, lambda { |test| test.title.present? }
62
+ end
63
+ end
64
+ @st = ScoringTest.new
65
+ @st.title = 'I have a title'
66
+ end
67
+
68
+ should "have the default scoring used" do
69
+ assert_equal 40, @st.completeness_score
70
+ end
71
+ end
72
+
73
+
74
+ context "A class with scoring defined with a symbol weighting" do
75
+ setup do
76
+ reset_class 'ScoringTest'
77
+ ScoringTest.class_eval do
78
+ define_completeness_scoring do
79
+ check :title, lambda { |test| test.title.present? }, :high
80
+ end
81
+ end
82
+ @st = ScoringTest.new
83
+ @st.title = 'I have a title'
84
+ end
85
+
86
+ should "have a scoring from the common_weightings hash used used" do
87
+ assert_equal ScoringTest.model_weightings[:high], @st.completeness_score
88
+ end
89
+ end
90
+
91
+
92
+ context "A class with scoring defined with a custom weighting" do
93
+ setup do
94
+ reset_class 'ScoringTest'
95
+ ScoringTest.class_eval do
96
+ define_completeness_scoring do
97
+ weightings :super_high => 80
98
+ check :title, lambda { |test| test.title.present? }, :super_high
99
+ end
100
+ end
101
+ @st = ScoringTest.new
102
+ @st.title = 'I have a title'
103
+ end
104
+
105
+ should "have a scoring from the common_weightings hash used used" do
106
+ assert_equal ScoringTest.model_weightings[:super_high], @st.completeness_score
107
+ end
108
+ end
109
+
110
+
111
+ context "A class with scoring defined with a custom weighting and no common weightings" do
112
+ setup do
113
+ reset_class 'ScoringTest'
114
+ ScoringTest.class_eval do
115
+ define_completeness_scoring do
116
+ weightings :super_high => 80 , :merge_with_common => false
117
+ check :title, lambda { |test| test.title.present? }, :super_high
118
+ end
119
+ end
120
+ @st = ScoringTest.new
121
+ @st.title = 'I have a title'
122
+ end
123
+
124
+ should "have a scoring from the common_weightings hash used used" do
125
+ assert_equal nil, ScoringTest.model_weightings[:high]
126
+ assert_equal ScoringTest.model_weightings[:super_high], @st.completeness_score
127
+ end
128
+ end
129
+
130
+
131
+ context "A class with scoring defined with a check using a symbol to a private method" do
132
+ setup do
133
+ reset_class 'ScoringTest'
134
+ ScoringTest.class_eval do
135
+ define_completeness_scoring do
136
+ check :title, :title_present?, :high
137
+ end
138
+
139
+ private
140
+ def title_present?
141
+ self.title.present?
142
+ end
143
+ end
144
+ @st = ScoringTest.new
145
+ @st.title = 'I have a title'
146
+ end
147
+
148
+ should "have a scoring from the common_weightings hash used used" do
149
+ assert_equal 1, @st.passed_checks.size
150
+ assert_equal 60, @st.completeness_score
151
+ end
152
+ end
153
+
154
+
155
+ context "A class with scoring defined and cache to field directive" do
156
+ setup do
157
+ reset_class 'ScoringTest'
158
+ ScoringTest.class_eval do
159
+ define_completeness_scoring do
160
+ cache_score :absolute
161
+ check :title, :title_present?, :high
162
+ end
163
+
164
+ private
165
+ def title_present?
166
+ self.title.present?
167
+ end
168
+ end
169
+ @st = ScoringTest.new
170
+ @st.title = 'I have a title'
171
+ end
172
+
173
+ should "have a before filter added, and save the defined calculation to the default field" do
174
+ assert_equal nil, @st.cached_completeness_score
175
+ @st.valid?
176
+ assert_equal @st.completeness_score, @st.cached_completeness_score
177
+ end
178
+ end
179
+
180
+
181
+ context "A class with scoring" do
182
+ setup do
183
+ reset_class 'ScoringTest'
184
+ ScoringTest.class_eval do
185
+ define_completeness_scoring do
186
+ check :title, lambda { |test| test.title.present? }, :high
187
+ end
188
+ end
189
+ @st = ScoringTest.new
190
+ end
191
+
192
+ should "have a grade of :high" do
193
+ assert_equal :poor, @st.completeness_grade
194
+ @st.title = 'I have a title'
195
+ assert_equal :high, @st.completeness_grade
196
+ end
197
+ end
198
+ end
Binary file
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: completeness-fu
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Kalderimis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-03 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.3.3
24
+ version:
25
+ description:
26
+ email: josh.kalderimis@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - VERSION.yml
35
+ - init.rb
36
+ - lib/completeness-fu.rb
37
+ - lib/completeness-fu/active_record_additions.rb
38
+ - lib/completeness-fu/scoring_builder.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/joshk/completeness-fu
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --charset=UTF-8
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.5
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Simple dsl for defining how to calculate how complete a model instance is (similar to LinkedIn profile completeness)
67
+ test_files:
68
+ - test/debug.log
69
+ - test/en.yml
70
+ - test/helper.rb
71
+ - test/schema.rb
72
+ - test/scoring_test.rb
73
+ - test/test.sqlite3