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.
- data/VERSION.yml +4 -0
- data/init.rb +1 -0
- data/lib/completeness-fu.rb +20 -0
- data/lib/completeness-fu/active_record_additions.rb +112 -0
- data/lib/completeness-fu/scoring_builder.rb +46 -0
- data/test/debug.log +1 -0
- data/test/en.yml +8 -0
- data/test/helper.rb +56 -0
- data/test/schema.rb +6 -0
- data/test/scoring_test.rb +198 -0
- data/test/test.sqlite3 +0 -0
- metadata +73 -0
data/VERSION.yml
ADDED
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
|
data/test/debug.log
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Logfile created on Wed Sep 23 14:40:19 +0200 2009 by logger.rb/22285
|
data/test/en.yml
ADDED
data/test/helper.rb
ADDED
@@ -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
|
data/test/schema.rb
ADDED
@@ -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
|
data/test/test.sqlite3
ADDED
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
|