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