├── .gitignore ├── .travis.yml ├── Appraisals ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── VERSION.yml ├── completeness-fu.gemspec ├── gemfiles ├── rails-3-1.gemfile └── rails-3.gemfile ├── lib ├── completeness-fu.rb └── completeness-fu │ ├── active_model_additions.rb │ └── scoring_builder.rb ├── readme.markdown └── test ├── en.yml ├── scoring_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | test/test.sqlite3 2 | pkg 3 | Gemfile.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1 7 | - 2.2 8 | - ruby-head 9 | - jruby 10 | - rbx 11 | 12 | gemfile: 13 | - gemfiles/rails-3.gemfile 14 | - gemfiles/rails-3-1.gemfile 15 | 16 | sudo: false -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-3" do 2 | gem "activemodel", "~> 3.0.0" 3 | end 4 | 5 | appraise "rails-3-1" do 6 | gem "activemodel", "~> 3.1.0" 7 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'activemodel', '~> 3.0' 4 | gem 'rake', '~> 0.8.7' 5 | gem 'shoulda', '~> 2.11.3' 6 | gem 'mocha', '~> 0.9.8' 7 | 8 | gem 'appraisal' 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activemodel (3.1.0) 5 | activesupport (= 3.1.0) 6 | bcrypt-ruby (~> 3.0.0) 7 | builder (~> 3.0.0) 8 | i18n (~> 0.6) 9 | activesupport (3.1.0) 10 | multi_json (~> 1.0) 11 | appraisal (0.3.8) 12 | bundler 13 | rake 14 | bcrypt-ruby (3.0.0) 15 | builder (3.0.0) 16 | i18n (0.6.0) 17 | mocha (0.9.12) 18 | multi_json (1.0.3) 19 | rake (0.8.7) 20 | shoulda (2.11.3) 21 | 22 | PLATFORMS 23 | ruby 24 | 25 | DEPENDENCIES 26 | activemodel (~> 3.0) 27 | appraisal 28 | mocha (~> 0.9.8) 29 | rake (~> 0.8.7) 30 | shoulda (~> 2.11.3) 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | begin 3 | require 'bundler/setup' 4 | require 'appraisal' 5 | rescue LoadError 6 | puts 'although not required, its recommended you use bundler during development' 7 | end 8 | 9 | require 'rake' 10 | 11 | Bundler::GemHelper.install_tasks 12 | 13 | desc 'Default: run unit tests.' 14 | task :default => :test 15 | 16 | require 'rake/testtask' 17 | Rake::TestTask.new(:test) do |t| 18 | t.pattern = 'test/**/*_test.rb' 19 | t.libs << "test" 20 | t.verbose = true 21 | end 22 | 23 | require 'rake/rdoctask' -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :patch: 2 3 | :major: 0 4 | :minor: 5 5 | -------------------------------------------------------------------------------- /completeness-fu.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{completeness-fu} 8 | s.version = "0.6.0" 9 | s.date = %q{2011-03-29} 10 | 11 | s.summary = %q{Simple dsl for defining how to calculate how complete a model instance is (similar to LinkedIn profile completeness)} 12 | s.description = s.summary 13 | s.homepage = %q{http://github.com/joshk/completeness-fu} 14 | 15 | s.authors = ["Josh Kalderimis"] 16 | s.email = %q{josh.kalderimis@gmail.com} 17 | 18 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 19 | 20 | s.extra_rdoc_files = ["readme.markdown"] 21 | s.rdoc_options = ["--charset=UTF-8"] 22 | s.rubygems_version = %q{1.3.6} 23 | 24 | s.require_paths = ['lib'] 25 | s.files = `git ls-files`.split("\n") 26 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 27 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 28 | 29 | if s.respond_to? :specification_version then 30 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 31 | s.specification_version = 3 32 | 33 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 34 | s.add_dependency(%q, ["~> 3.0"]) 35 | s.add_development_dependency(%q, ["~> 0.8.7"]) 36 | s.add_development_dependency(%q, ["~> 2.11.3"]) 37 | s.add_development_dependency(%q, ["~> 0.9.8"]) 38 | else 39 | s.add_dependency(%q, ["~> 3.0"]) 40 | s.add_dependency(%q, ["~> 0.8.7"]) 41 | s.add_dependency(%q, ["~> 2.11.3"]) 42 | s.add_dependency(%q, ["~> 0.9.8"]) 43 | end 44 | else 45 | s.add_dependency(%q, ["~> 3.0"]) 46 | s.add_dependency(%q, ["~> 0.8.7"]) 47 | s.add_dependency(%q, ["~> 2.11.3"]) 48 | s.add_dependency(%q, ["~> 0.9.8"]) 49 | end 50 | end 51 | 52 | -------------------------------------------------------------------------------- /gemfiles/rails-3-1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "activemodel", "~> 3.1.0" 6 | gem "rake", "~> 0.8.7" 7 | gem "shoulda", "~> 2.11.3" 8 | gem "mocha", "~> 0.9.8" 9 | gem "appraisal" 10 | 11 | -------------------------------------------------------------------------------- /gemfiles/rails-3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "activemodel", "~> 3.0.0" 6 | gem "rake", "~> 0.8.7" 7 | gem "shoulda", "~> 2.11.3" 8 | gem "mocha", "~> 0.9.8" 9 | gem "appraisal" 10 | 11 | -------------------------------------------------------------------------------- /lib/completeness-fu.rb: -------------------------------------------------------------------------------- 1 | module CompletenessFu 2 | class CompletenessFuError < StandardError; end 3 | 4 | class << self 5 | attr_accessor :common_weightings 6 | attr_accessor :default_weighting 7 | attr_accessor :default_i18n_namespace 8 | attr_accessor :default_gradings 9 | end 10 | 11 | self.common_weightings = { :low => 20, :medium => 40, :high => 60 } 12 | 13 | self.default_weighting = 40 14 | 15 | self.default_i18n_namespace = [:completeness_scoring, :models] 16 | 17 | self.default_gradings = { 18 | :poor => 0..24, 19 | :low => 25..49, 20 | :medium => 50..79, 21 | :high => 80..100 22 | } 23 | 24 | def self.setup_orm(orm_class) 25 | orm_class.class_eval { include CompletenessFu::ActiveModelAdditions } 26 | end 27 | 28 | end 29 | 30 | require 'completeness-fu/active_model_additions' 31 | require 'completeness-fu/scoring_builder' 32 | 33 | 34 | CompletenessFu.setup_orm(::ActiveRecord::Base) if defined?(::ActiveRecord::Base) 35 | CompletenessFu.setup_orm(::CassandraObject::Base) if defined?(::CassandraObject::Base) 36 | CompletenessFu.setup_orm(::Mongoid::Document) if defined?(::Mongoid::Document) 37 | -------------------------------------------------------------------------------- /lib/completeness-fu/active_model_additions.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/class/inheritable_attributes' 2 | require 'active_support/core_ext/class/attribute_accessors' 3 | 4 | module CompletenessFu 5 | 6 | module ActiveModelAdditions 7 | 8 | def self.included(base) 9 | base.class_eval do 10 | def self.define_completeness_scoring(&checks_block) 11 | unless self.respond_to?(:model_name) 12 | raise CompletenessFuError, 'please make sure ActiveModel::Naming is included so completeness_scoring can translate messages correctly, or that you implement a model_name method.' 13 | end 14 | 15 | class_attribute :completeness_checks 16 | cattr_accessor :default_weighting 17 | cattr_accessor :model_weightings 18 | 19 | self.send :extend, ClassMethods 20 | self.send :include, InstanceMethods 21 | 22 | checks_results = CompletenessFu::ScoringBuilder.generate(self, &checks_block) 23 | 24 | self.default_weighting = checks_results[:default_weighting] 25 | self.completeness_checks = checks_results[:completeness_checks] 26 | self.model_weightings = checks_results[:model_weightings] 27 | 28 | if checks_results[:cache_score_details] 29 | unless self.include?(ActiveModel::Validations::Callbacks) 30 | raise CompletenessFuError, 'please make sure ActiveModel::Validations::Callbacks is included before define_completeness_scoring if you want to cache competeness scoring' 31 | end 32 | self.before_validation checks_results[:cache_score_details] 33 | end 34 | end 35 | end 36 | end 37 | 38 | 39 | module ClassMethods 40 | def max_completeness_score 41 | self.completeness_checks.inject(0) { |score, check| score += check[:weighting] } 42 | end 43 | end 44 | 45 | 46 | module InstanceMethods 47 | # returns an array of hashes with the translated name, description + weighting 48 | def failed_checks 49 | all_checks_which_pass(false) 50 | end 51 | 52 | # returns an array of hashes with the translated name, description + weighting 53 | def passed_checks 54 | all_checks_which_pass 55 | end 56 | 57 | # returns the absolute complete score 58 | def completeness_score 59 | sum_score = 0 60 | passed_checks.each { |check| sum_score += check[:weighting] } 61 | sum_score 62 | end 63 | 64 | # returns the percentage of completeness (relative score) 65 | def percent_complete 66 | self.completeness_score.to_f / self.class.max_completeness_score.to_f * 100 67 | end 68 | 69 | # returns a basic 'grading' based on percent_complete, defaults are :high, :medium, :low, and :poor 70 | def completeness_grade 71 | CompletenessFu.default_gradings.each do |grading| 72 | return grading.first if grading.last.include?(self.percent_complete.round) 73 | end 74 | raise CompletenessFuError, "grade could not be determined with percent complete #{self.percent_complete.round}" 75 | end 76 | 77 | 78 | private 79 | 80 | def all_checks_which_pass(should_pass = true) 81 | self.completeness_checks.inject([]) do |results, check| 82 | check_result = run_check(check[:check]) 83 | results << translate_check_details(check) if (should_pass ? check_result : !check_result) 84 | results 85 | end 86 | end 87 | 88 | def run_check(check) 89 | case check 90 | when Proc 91 | return check.call(self) 92 | when Symbol 93 | return self.send check 94 | else 95 | raise CompletenessFuError, "check of type #{check.class} not acceptable" 96 | end 97 | end 98 | 99 | def translate_check_details(full_check) 100 | namespace = CompletenessFu.default_i18n_namespace + [self.class.model_name.underscore.to_sym, full_check[:name]] 101 | 102 | translations = [:title, :description, :extra].inject({}) do |list, field| 103 | list[field] = I18n.t(field.to_sym, :scope => namespace) 104 | list 105 | end 106 | 107 | full_check.merge(translations) 108 | end 109 | 110 | def cache_completeness_score(score_type) 111 | score = case score_type 112 | when :relative 113 | self.percent_complete 114 | when :absolute 115 | self.completeness_score 116 | else 117 | raise ArgumentException, 'completeness scoring type not recognized' 118 | end 119 | self.cached_completeness_score = score.round 120 | true 121 | end 122 | end 123 | 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /lib/completeness-fu/scoring_builder.rb: -------------------------------------------------------------------------------- 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_weighting 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 -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | Completeness-Fu [![Build Status](https://secure.travis-ci.org/joshk/completeness-fu.png?branch=master)](http://travis-ci.org/joshk/completeness-fu) 2 | =============== 3 | 4 | In short, completeness-fu for ActiveModel allows you cleanly define the way a model instance is scored for completeness, similar to LinkedIn user profiles. 5 | 6 | 7 | When should I use it? 8 | --------------------- 9 | 10 | When you want your objects/models/instances to be of a certain standard, if it be data presence, format or what values are allowed and not allowed, 11 | before an object can be saved or updated then you use validations. But when you want to allow more information to be entered, thus relaxing some 12 | of the validation rules and prompt the user to enrich the data, then completeness-fu is your cup of tea. 13 | 14 | Take an events web site for example, you may want to import information from various sources which might have varied data quality. 15 | If your validations are too strict then the information won't import, but if you relax your validations too much then you risk 16 | having quantity but not quality. What you can do is relax the validations and add some quality checks which calculate a score 17 | which is then used to determine if the event information is shown on the public site or is listed in the admin panel prompting 18 | staff to enrich the data before it is made public. 19 | 20 | 21 | What does it work with? 22 | ----------------------- 23 | 24 | Completeness-Fu used to be for ActiveRecord only, but this has changed with the latest version with only ActiveModel needed as a gem dependency (and a little ActiveSupport). This means that the latest version works with the following ORMs : 25 | 26 | - ActiveRecord 27 | 28 | - Mongoid 29 | 30 | - Cassandra Object 31 | 32 | The `define_completeness_scoring` method is mixed into the above mentioned ORMs by default, but if you want to use Completeness-Fu in an ActiveModel model which isn't one of the above ORMs, make sure you include `ActiveModel::Naming`. If you want to cache the completeness score make sure you also include `ActiveModel::Validations` and `ActiveModel::Validations::Callbacks`. 33 | 34 | 35 | How do I use it? 36 | ---------------- 37 | 38 | If you want your model to only be regarded as complete if it has a title, description, and picture, you can do the following: 39 | 40 | class Example < ActiveRecord::Base 41 | define_completeness_scoring do 42 | check :title, lambda { |per| per.title.present? }, :high # => defaults to 60 43 | check :description, lambda { |per| per.description.present? }, :medium # => defaults to 40 44 | check :main_image, lambda { |per| per.main_image? }, :low # => defaults to 20 45 | end 46 | end 47 | 48 | Now, you can access several methods to find out how far along a model is : _passed\_checks_, _failed\_checks_, _completeness\_score_, _percent\_complete_. 49 | 50 | @example = Example.new 51 | 52 | @example.passed_checks.size # => 0 53 | @example.failed_checks.size # => 3 54 | 55 | @example.completeness_score # => 0 56 | @example.percent_complete # => 0 57 | 58 | @example.title = "An Awesome Example" 59 | @example.passed_checks.size # => 1 60 | @example.failed_checks.size # => 2 61 | 62 | @example.completeness_score # => 60 63 | @example.percent_complete # => 50 64 | 65 | 66 | Options 67 | ------- 68 | 69 | You can add the following to an initializer to set some defaults: 70 | 71 | CompletenessFu.common_weights = { :low => 30, :medium => 50, :high => 70 } 72 | 73 | CompletenessFu.default_weighting = :medium 74 | 75 | `weights` and `default_weighting` can be changed per model, and you can use symbols for the checks instead of lambdas, thus allowing you to place check logic as methods in your class (public or private). 76 | 77 | define_completeness_scoring do 78 | weights :low => 30, :super_high => 60 79 | default_weighting :medium 80 | 81 | check :title, lambda { |per| per.title.present? }, :super_high 82 | check :description, :description_present? 83 | check :main_image, :main_image_or_pretty_picture?, :low 84 | end 85 | 86 | And if you want to cache the score to a field so you can use it in database searches you just have to add the following: 87 | 88 | define_completeness_scoring do 89 | cache_score :absolute # the default is :relative 90 | 91 | check :title, lambda { |per| per.title.present? }, :super_high 92 | check :description, :description_present? 93 | end 94 | 95 | Requirements 96 | ------------ 97 | 98 | If you are not using one of the above ORMs, make sure `ActiveModel::Validations` and `ActiveModel::Validations::Callbacks` are included. 99 | 100 | 101 | Passed checks, Failed checks and i18n 102 | ---------------------------------------- 103 | 104 | Both the _passed\_checks_ and _failed\_checks_ methods return an array of hashes with extended information about the check. 105 | The check hash includes a translated title, description and extra information which is based on the name of the check. 106 | The translation structure is as such: 107 | 108 | en: 109 | completeness_scoring: 110 | models: 111 | my_model_name: 112 | name_of_check: 113 | title: 'Title' 114 | description: 'The Check Description' 115 | extra: 'Extra Info' 116 | 117 | 118 | Up and coming features 119 | ---------------------- 120 | 121 | - enhance caching so filter type can be changed and field to save score to can be customized 122 | - ability to 'share' common lambdas 123 | - better docs 124 | - increase test coverage (error edge cases, ORM inclusion) 125 | 126 | 127 | Change Log 128 | ---------- 129 | 130 | 4 Sep 10 131 | 132 | - added bundler for development and testing 133 | - make the plugin ORM agnostic, only ActiveModel is required 134 | 135 | 15 Oct 09 136 | 137 | - bug fix for failed checks when check is a symbol to a method 138 | - bug fix for grading (percent complete needed to be rounded) 139 | - change of CompletenessFu.default\_weightings to CompletenessFu.default\_weighting 140 | - change of CompletenessFu.default\_grading to CompletenessFu.default\_gradings 141 | 142 | 29 Sep 09 143 | 144 | - added a 'grading' method which returns either :poor, :low, :medium or :high based on its percentage complete 145 | - added ability to customize the default translations namespace using CompletenessFu.default\_i18n\_namespace 146 | 147 | 28 Sep 09 148 | 149 | - move the scoring check builder into its own class so that it works within a clean room 150 | 151 | 24 Sep 09 152 | 153 | - options to save the score to a field (caching) - good for searching on 154 | - define methods on the class to use in the checks 155 | 156 | 157 | 158 | Contributors 159 | ------------ 160 | 161 | - Peter (pero-ict) 162 | - Andrew Brown (omenking) 163 | - Paul Campbell (paulca) 164 | -------------------------------------------------------------------------------- /test/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | completeness_scoring: 3 | models: 4 | scoring_test: 5 | title: 6 | title: 'Title' 7 | description: 'The Scoring Test Description' 8 | extra: 'Just give it a title' -------------------------------------------------------------------------------- /test/scoring_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | class ScoringTest < Test::Unit::TestCase 5 | 6 | context "A class which includes ActiveModel" do 7 | setup { rebuild_class('ScoringTest') } 8 | 9 | should "have a define_completeness_scoring mixed in" do 10 | assert ScoringTest.methods.map(&:to_s).include?('define_completeness_scoring') 11 | end 12 | end 13 | 14 | context "A class with scoring defined" do 15 | setup do 16 | klass = rebuild_class('ScoringTest') 17 | klass.class_eval do 18 | define_completeness_scoring do 19 | check :title, lambda { |test| test.title.present? }, 20 20 | end 21 | end 22 | end 23 | 24 | should "have one scoring check" do 25 | assert_equal 1, ScoringTest.completeness_checks.size 26 | end 27 | 28 | should "have one failed check" do 29 | st = ScoringTest.new 30 | assert_equal 1, st.failed_checks.size 31 | end 32 | 33 | context "and with one complete check" do 34 | setup do 35 | @st = ScoringTest.new 36 | @st.title = 'I have a title' 37 | end 38 | 39 | should "have an absolute completeness score of 20" do 40 | assert_equal 20, @st.completeness_score 41 | end 42 | 43 | should "have a relative completeness score of 0 (percent complete)" do 44 | assert_equal 100, @st.percent_complete 45 | end 46 | 47 | should "have a description" do 48 | assert_equal "The Scoring Test Description", @st.passed_checks.first[:description] 49 | end 50 | end 51 | end 52 | 53 | context "A class with scoring defined with no weighting" do 54 | setup do 55 | klass = rebuild_class('ScoringTest') 56 | klass.class_eval do 57 | define_completeness_scoring do 58 | check :title, lambda { |test| test.title.present? } 59 | end 60 | end 61 | @st = klass.new 62 | @st.title = 'I have a title' 63 | end 64 | 65 | should "have the default scoring used" do 66 | assert_equal 40, @st.completeness_score 67 | end 68 | end 69 | 70 | context "A class with scoring defined with a symbol weighting" do 71 | setup do 72 | klass = rebuild_class('ScoringTest') 73 | klass.class_eval do 74 | define_completeness_scoring do 75 | check :title, lambda { |test| test.title.present? }, :high 76 | end 77 | end 78 | @st = klass.new 79 | @st.title = 'I have a title' 80 | end 81 | 82 | should "have a scoring from the common_weightings hash used used" do 83 | assert_equal ScoringTest.model_weightings[:high], @st.completeness_score 84 | end 85 | end 86 | 87 | context "A class with scoring defined with a custom weighting" do 88 | setup do 89 | klass = rebuild_class('ScoringTest') 90 | klass.class_eval do 91 | define_completeness_scoring do 92 | weightings :super_high => 80 93 | check :title, lambda { |test| test.title.present? }, :super_high 94 | end 95 | end 96 | @st = klass.new 97 | @st.title = 'I have a title' 98 | end 99 | 100 | should "have a scoring from the common_weightings hash used used" do 101 | assert_equal ScoringTest.model_weightings[:super_high], @st.completeness_score 102 | end 103 | end 104 | 105 | context "A class with scoring defined with a custom weighting and no common weightings" do 106 | setup do 107 | klass = rebuild_class('ScoringTest') 108 | klass.class_eval do 109 | define_completeness_scoring do 110 | weightings :super_high => 80 , :merge_with_common => false 111 | check :title, lambda { |test| test.title.present? }, :super_high 112 | end 113 | end 114 | @st = klass.new 115 | @st.title = 'I have a title' 116 | end 117 | 118 | should "have a scoring from the common_weightings hash used used" do 119 | assert_equal nil, ScoringTest.model_weightings[:high] 120 | assert_equal ScoringTest.model_weightings[:super_high], @st.completeness_score 121 | end 122 | end 123 | 124 | context "A class with scoring defined with a check using a symbol to a private method" do 125 | setup do 126 | klass = rebuild_class('ScoringTest') 127 | klass.class_eval do 128 | define_completeness_scoring do 129 | check :title, :title_present?, :high 130 | check :name, :name_present?, :high 131 | end 132 | 133 | private 134 | 135 | def title_present? 136 | self.title.present? 137 | end 138 | 139 | def name_present? 140 | false 141 | end 142 | end 143 | @st = klass.new 144 | @st.title = 'I have a title' 145 | end 146 | 147 | should "have the correct failing checks" do 148 | assert_equal 1, @st.passed_checks.size 149 | assert_equal 1, @st.failed_checks.size 150 | assert_equal 60, @st.completeness_score 151 | assert_equal 50.0, @st.percent_complete 152 | end 153 | 154 | should "have a scoring from the common_weightings hash used used" do 155 | assert_equal 1, @st.passed_checks.size 156 | assert_equal 60, @st.completeness_score 157 | end 158 | 159 | end 160 | 161 | context "A class with scoring defined and cache to field directive" do 162 | setup do 163 | klass = rebuild_class('ScoringTest') 164 | klass.class_eval do 165 | define_completeness_scoring do 166 | cache_score :absolute 167 | check :title, :title_present?, :high 168 | end 169 | 170 | attr_accessor :cached_completeness_score 171 | 172 | private 173 | def title_present? 174 | self.title.present? 175 | end 176 | end 177 | @st = klass.new 178 | @st.title = 'I have a title' 179 | end 180 | 181 | should "have a before filter added, and save the defined calculation to the default field" do 182 | assert_equal nil, @st.cached_completeness_score 183 | @st.valid? 184 | assert_equal @st.completeness_score, @st.cached_completeness_score 185 | end 186 | end 187 | 188 | context "A class with scoring" do 189 | setup do 190 | klass = rebuild_class('ScoringTest') 191 | klass.class_eval do 192 | define_completeness_scoring do 193 | check :title, lambda { |test| test.title.present? }, :high 194 | end 195 | end 196 | @st = klass.new 197 | end 198 | 199 | should "have a grade of :high" do 200 | assert_equal :poor, @st.completeness_grade 201 | @st.title = 'I have a title' 202 | assert_equal :high, @st.completeness_grade 203 | end 204 | end 205 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'although not required, its recommended you use bundler when running the tests' 5 | end 6 | 7 | require 'test/unit' 8 | require 'shoulda' 9 | require 'mocha' 10 | 11 | require 'active_model' 12 | 13 | require 'completeness-fu' 14 | 15 | 16 | ROOT = File.join(File.dirname(__FILE__), '..') 17 | 18 | I18n.load_path << File.join(ROOT, 'test', 'en.yml') 19 | 20 | 21 | def rebuild_class(class_name) 22 | Object.send(:remove_const, class_name) rescue nil 23 | 24 | klass = Object.const_set(class_name, Class.new) 25 | 26 | klass.class_eval do 27 | extend ActiveModel::Naming 28 | include ActiveModel::Validations 29 | include ActiveModel::Validations::Callbacks 30 | 31 | include CompletenessFu::ActiveModelAdditions 32 | 33 | attr_accessor :title 34 | end 35 | 36 | klass 37 | end --------------------------------------------------------------------------------