completeness-fu 0.5.0

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