├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── activerecord-reputation-system.gemspec ├── lib ├── activerecord-reputation-system.rb ├── generators │ └── reputation_system │ │ ├── reputation_system_generator.rb │ │ └── templates │ │ ├── add_data_to_evaluations.rb │ │ ├── add_data_to_reputations.rb │ │ ├── add_evaluations_index.rb │ │ ├── add_reputation_messages_index.rb │ │ ├── add_reputations_index.rb │ │ ├── change_evaluations_index_to_unique.rb │ │ ├── change_reputation_messages_index_to_unique.rb │ │ ├── change_reputations_index_to_unique.rb │ │ └── create_reputation_system.rb ├── reputation_system.rb └── reputation_system │ ├── base.rb │ ├── evaluation_methods.rb │ ├── finder_methods.rb │ ├── models │ ├── evaluation.rb │ ├── reputation.rb │ └── reputation_message.rb │ ├── network.rb │ ├── query_builder.rb │ ├── query_methods.rb │ ├── reputation_methods.rb │ ├── scope_methods.rb │ └── version.rb └── spec ├── reputation_system ├── base_spec.rb ├── evaluation_methods_spec.rb ├── finder_methods_spec.rb ├── models │ ├── evaluation_spec.rb │ ├── reputation_message_spec.rb │ └── reputation_spec.rb ├── query_methods_spec.rb ├── reputation_methods_spec.rb └── scope_methods_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg 3 | *.swp 4 | *.DS_Store 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0 5 | - 2.1 6 | - 2.2 7 | before_install: 8 | - gem --version 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.1 (November 27, 2014) 2 | 3 | * Remove `protected_attributes` to fix a Rails 4 compatibility. 4 | 5 | # 3.0.0 (October 7, 2014) 6 | 7 | * Add ability to set custom aggregation functions. (Caio Almeida) 8 | 9 | * Add serialized data field to evaluation and reputation models. (Caio Almeida) 10 | 11 | * Make ActiveRecord 4 compatible. 12 | 13 | * Drop Rails 3 and Ruby 1.8 support. 14 | 15 | # 2.0.2 (December 1, 2012) 16 | 17 | * Fix a bug associated with `add_or_update_evaluation` method that happens when 18 | source uses STI. 19 | 20 | # 2.0.1 (October 5, 2012) 21 | 22 | * Print out future deprecation warning for methods `with_reputation` and 23 | `with_normalized_reputation`. 24 | 25 | * Fix a finder related bug. 26 | 27 | # 2.0.0 (October 5, 2012) 28 | 29 | * Deprecate `init_value` option. 30 | 31 | * Fix a average computation bug associated with deletes. 32 | 33 | * `delete_evaluation` returns false on failure, instead of nil. 34 | 35 | * Add `has_evaluation?` method. 36 | 37 | * Add auto-require `reputation_system`. 38 | 39 | * Add `evaluators_for` method. 40 | 41 | * Deprecate `reputation_value_for` and `normalized_reputation_value_for` 42 | methods. 43 | 44 | * Add `evaluations` association for all evaluation targets. 45 | 46 | * Set `:sum` as default for `aggregated_by` option. 47 | 48 | * Rename models - RSReputation to ReputationSystem::Reputation, RSEvaluation to 49 | ReputationSystem::Evaluation and RSReputationMessage to 50 | ReputationSystem::ReputationMessage 51 | 52 | # 1.5.1 (October 4, 2012) 53 | 54 | * Fix a bug that raises exception when associations related reputation 55 | propageted has not been initialized at that time. 56 | 57 | # 1.5.0 (September 15, 2012) 58 | 59 | * Add a support for STI. 60 | 61 | * Add `reputation_for` and `normalized_reputation_for` methods that are shorten 62 | name of `reputation_value_for` and `normalized_reputation_value_for` methods. 63 | 64 | # 1.4.0 (September 10, 2012) 65 | 66 | * Add `with_normalized_reputation` and `with_normalized_reputation_only`. 67 | 68 | * Add `with_reputation` and `with_reputation_only` methods. 69 | 70 | # 1.3.4 (August 9, 2012) 71 | 72 | * Fix name of a migration class again. 73 | 74 | # 1.3.3 (August 8, 2012) 75 | 76 | * Fix name of a migration class. 77 | 78 | # 1.3.2 (August 8, 2012) 79 | 80 | * Add migration files. 81 | 82 | # 1.3.1 (August 8, 2012) 83 | 84 | * Make index unique. 85 | 86 | # 1.3.0 (August 1, 2012) 87 | 88 | * Add `evaluated_by method`. 89 | 90 | * Make evaluation methods return true on success. 91 | 92 | # 1.2.1 (July 14, 2012) 93 | 94 | * Fix index names to be able to `db:rollback` the migrations. (Amr Tamimi) 95 | 96 | # 1.2.0 (June 12, 2012) 97 | 98 | * Fix race conditions with uniqueness validations. 99 | 100 | # 1.1.0 (May 22, 2012) 101 | 102 | * Add `increase_evaluation` and `decrease_evaluation` methods. 103 | 104 | * Fix `add_or_update_evaluation` bug when using scope. 105 | 106 | * Fix README bugs. (Eli Fox-Epstein) 107 | 108 | # 1.0.0 (May 17, 2012) 109 | 110 | * Open sourced to the world! 111 | 112 | * Sanitize all sql statements in query.rb. 113 | 114 | * Add validations for reputation messages. 115 | 116 | * Rename spec gem. 117 | 118 | * Overwrite existing reputation definitions instead of raising exceptions. 119 | 120 | * Rename `reputation_system` to `reputation_system_active_record`. 121 | 122 | * Support initial value. 123 | 124 | * Support for default `source_of` attribute. 125 | 126 | * Change gem name from `reputation-system` to `reputation_system`. 127 | 128 | * No more active record models export upon reputation system generation. 129 | 130 | * Remove rails init files. 131 | 132 | * Major refactoring. 133 | 134 | * Rename `normalize` to `active`. 135 | 136 | * Fix Query bug. 137 | 138 | * Remove `ExternalSource` support. 139 | 140 | * Add `rank_for` method. 141 | 142 | * Add count query interface. 143 | 144 | * Organize Rakefile more nicely. 145 | 146 | * Organize the gem more nicely. 147 | 148 | * Add non strict version of `delete_evaluation` method. 149 | 150 | * Fix rails 3.2 issue 151 | 152 | * Stop using transaction. 153 | 154 | * Really make ActiveRecord 3 compatible 155 | 156 | * Make ActiveRecord 3 compatible 157 | 158 | * Add a method to check if a reputation is included for normalization. 159 | 160 | * Improve Generator. 161 | 162 | * Allow reputation to be inactive so that it will not count into the normalized 163 | value. 164 | 165 | * Destroy dependent reputations and reputation messages. 166 | 167 | * Add method to output sql statement for querying. 168 | 169 | * Add normalized value support for querying. 170 | 171 | * Add scope support for querying. 172 | 173 | * Removing dependencies. 174 | 175 | * Fix `instance_exec` error. 176 | 177 | * Add query interface. 178 | 179 | * Use transaction for better performance. 180 | 181 | * Fix a bug related to `add_or_update_evaluation`. 182 | 183 | * Add normalized reputation value accessor. 184 | 185 | * Rename all models for organization and for a patch to deal with bug in class 186 | caching. 187 | 188 | * Add default value (:self) for `:of` attributes. Fix scope bug. Add support for 189 | non-array `:source_of` value. 190 | 191 | * Add support for scoping reputations. 192 | 193 | * Major redesign of the framework. Now supports "Multiple level" of reputation 194 | relationship. 195 | 196 | * First Iteration with minimum capability. Only supporting "One level" of 197 | reputation relationship. 198 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ActiveRecord Reputation System [![Build Status](https://travis-ci.org/twitter/activerecord-reputation-system.svg?branch=master)](https://travis-ci.org/twitter/activerecord-reputation-system) [![Code Climate](https://codeclimate.com/github/twitter/activerecord-reputation-system/badges/gpa.svg)](https://codeclimate.com/github/twitter/activerecord-reputation-system) 2 | 3 | The Active Record Reputation System helps you build the reputation system for your Rails application. It allows Active Record to have reputations and get evaluated by other records. This gem allows you to: 4 | * define reputations in easily readable way. 5 | * integrate reputation systems into applications and decouple the system from the main application. 6 | * discover more about your application and make better decisions. 7 | 8 | ## Installation 9 | 10 | * If you are updating to version 2 from version older, you should check out [migration guide](https://github.com/twitter/activerecord-reputation-system/wiki/Migrate-to-Version-2.0). 11 | 12 | * **For Rails 3 use versions 2.0.2 and older.** 13 | 14 | Add to Gemfile: 15 | 16 | ```ruby 17 | gem 'activerecord-reputation-system' 18 | ``` 19 | 20 | Run: 21 | 22 | ```ruby 23 | bundle install 24 | rails generate reputation_system 25 | rake db:migrate 26 | ``` 27 | 28 | * Please do the installation on every upgrade as it may include new migration files. 29 | 30 | ## Quick Start 31 | 32 | Let's say we want to keep track of user karma in Q&A site where user karma is sum of questioning skill and answering skill. Questioning skill is sum of votes for user's questions and Answering skill is sum of average rating of user's answers. This can be defined as follow: 33 | ```ruby 34 | class User < ActiveRecord::Base 35 | has_many :answers 36 | has_many :questions 37 | 38 | has_reputation :karma, 39 | :source => [ 40 | { :reputation => :questioning_skill, :weight => 0.8 }, 41 | { :reputation => :answering_skill }] 42 | 43 | has_reputation :questioning_skill, 44 | :source => { :reputation => :votes, :of => :questions } 45 | 46 | has_reputation :answering_skill, 47 | :source => { :reputation => :avg_rating, :of => :answers } 48 | end 49 | 50 | class Answer < ActiveRecord::Base 51 | belongs_to :user, :as => :author 52 | 53 | has_reputation :avg_rating, 54 | :source => :user, 55 | :aggregated_by => :average, 56 | :source_of => [{ :reputation => :answering_skill, :of => :author }] 57 | end 58 | 59 | 60 | class Question < ActiveRecord::Base 61 | belongs_to :user 62 | 63 | has_reputation :votes, 64 | :source => :user 65 | end 66 | ``` 67 | 68 | Once reputation system is defined, evaluations for answers and questions can be added as follow: 69 | ```ruby 70 | @answer.add_evaluation(:avg_rating, 3, @user) 71 | @question.add_evaluation(:votes, 1, @user) 72 | ``` 73 | 74 | Reputation value can be accessed as follow: 75 | ```ruby 76 | @answer.reputation_for(:avg_rating) #=> 3 77 | @question.reputation_for(:votes) #=> 1 78 | @user.reputation_for(:karma) 79 | ``` 80 | 81 | You can query for records using reputation value: 82 | ```ruby 83 | User.find_with_reputation(:karma, :all, { :condition => 'karma > 10' }) 84 | ``` 85 | 86 | You can get source records that have evaluated the target record: 87 | ```ruby 88 | @question.evaluators_for(:votes) #=> [@user] 89 | ``` 90 | 91 | You can get target records that have been evaluated by a given source record: 92 | ```ruby 93 | Question.evaluated_by(:votes, @user) #=> [@question] 94 | ``` 95 | 96 | To use a custom aggregation function you need to provide the name of the method 97 | on the `:aggregated_by option`, and implement this method on the model. 98 | On the example below, our aggregation function sums all values and multiply by ten: 99 | ```ruby 100 | class Answer < ActiveRecord::Base 101 | belongs_to :author, :class_name => 'User' 102 | belongs_to :question 103 | 104 | has_reputation :custom_rating, 105 | :source => :user, 106 | :aggregated_by => :custom_aggregation 107 | 108 | def custom_aggregation(*args) 109 | rep, source, weight = args[0..2] 110 | 111 | # Ruby doesn't support method overloading, so let's handle parameters on a condition 112 | 113 | # For a new source, these are the input parameters: 114 | # rep, source, weight 115 | if args.length == 3 116 | rep.value + weight * source.value * 10 117 | 118 | # For an updated source, these are the input parameters: 119 | # rep, source, weight, oldValue, newSize 120 | elsif args.length == 5 121 | oldValue, newSize = args[3..4] 122 | rep.value + (source.value - oldValue) * 10 123 | end 124 | end 125 | end 126 | ``` 127 | 128 | ## Documentation 129 | 130 | Please refer [Wiki](https://github.com/twitter/activerecord-reputation-system/wiki) for available APIs and more information. 131 | 132 | ## Authors 133 | 134 | Katsuya Noguchi 135 | * [http://twitter.com/kn](http://twitter.com/kn) 136 | * [http://github.com/kn](http://github.com/kn) 137 | 138 | ## Related Links 139 | 140 | * RailsCasts: http://railscasts.com/episodes/364-active-record-reputation-system 141 | * Inspired by ["Building Web Reputation Systems" by Randy Farmer and Bryce Glass](http://shop.oreilly.com/product/9780596159801.do) 142 | 143 | ## Versioning 144 | 145 | For transparency and insight into our release cycle, releases will be numbered with the follow format: 146 | 147 | `..` 148 | 149 | And constructed with the following guidelines: 150 | 151 | * Breaking backwards compatibility bumps the major 152 | * New additions without breaking backwards compatibility bumps the minor 153 | * Bug fixes and misc changes bump the patch 154 | 155 | For more information on semantic versioning, please visit http://semver.org/. 156 | 157 | ## License 158 | 159 | Copyright 2012 Twitter, Inc. 160 | 161 | Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 162 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rdoc/task' 2 | require 'rubygems' 3 | require 'rubygems/package_task' 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Default: run specs.' 7 | task :default => :rspec 8 | 9 | desc 'Run the specs' 10 | RSpec::Core::RakeTask.new(:rspec) do |t| 11 | t.rspec_opts = ['--color'] 12 | t.pattern = './spec/**/*_spec.rb' 13 | end 14 | 15 | spec = Gem::Specification.load("#{File.dirname(__FILE__)}/activerecord-reputation-system.gemspec") 16 | 17 | desc "Package gem." 18 | Gem::PackageTask.new(spec) do |pkg| 19 | pkg.gem_spec = spec 20 | end 21 | -------------------------------------------------------------------------------- /activerecord-reputation-system.gemspec: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'lib', 'reputation_system', 'version') 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "activerecord-reputation-system" 5 | s.version = ::ReputationSystem::VERSION 6 | s.authors = ["Katsuya Noguchi"] 7 | s.email = ["katsuya@twitter.com"] 8 | s.homepage = "https://github.com/twitter/activerecord-reputation-system" 9 | s.description = "ActiveRecord Reputation System gem allows rails apps to compute and publish reputation scores for active record models." 10 | 11 | s.platform = Gem::Platform::RUBY 12 | s.has_rdoc = true 13 | s.summary = "ActiveRecord Reputation System gem allows rails apps to compute and publish reputation scores for active record models" 14 | 15 | s.add_development_dependency 'activerecord', '~> 4.0' 16 | s.add_development_dependency 'rake', ">=0.8.7" 17 | s.add_development_dependency 'rspec', '~> 3.1' 18 | s.add_development_dependency 'rdoc' 19 | s.add_development_dependency 'database_cleaner', "~> 1.2.0" 20 | s.add_development_dependency 'sqlite3', "~>1.3.5" 21 | 22 | s.require_path = 'lib' 23 | s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{lib,spec}/**/*") 24 | end 25 | -------------------------------------------------------------------------------- /lib/activerecord-reputation-system.rb: -------------------------------------------------------------------------------- 1 | require 'reputation_system' 2 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/reputation_system_generator.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'rails/generators' 18 | require 'rails/generators/migration' 19 | require 'rails/generators/active_record' 20 | 21 | class ReputationSystemGenerator < Rails::Generators::Base 22 | include Rails::Generators::Migration 23 | 24 | desc "Creates migration files required by reputation system gem." 25 | 26 | self.source_paths << File.join(File.dirname(__FILE__), 'templates') 27 | 28 | def self.next_migration_number(path) 29 | ActiveRecord::Generators::Base.next_migration_number(path) 30 | end 31 | 32 | def create_migration_files 33 | create_migration_file_if_not_exist 'create_reputation_system' 34 | create_migration_file_if_not_exist 'add_reputations_index' 35 | create_migration_file_if_not_exist 'add_evaluations_index' 36 | create_migration_file_if_not_exist 'add_reputation_messages_index' 37 | create_migration_file_if_not_exist 'change_evaluations_index_to_unique' 38 | create_migration_file_if_not_exist 'change_reputation_messages_index_to_unique' 39 | create_migration_file_if_not_exist 'change_reputations_index_to_unique' 40 | create_migration_file_if_not_exist 'add_data_to_reputations' 41 | create_migration_file_if_not_exist 'add_data_to_evaluations' 42 | end 43 | 44 | private 45 | 46 | def create_migration_file_if_not_exist(file_name) 47 | unless self.class.migration_exists?(File.dirname(File.expand_path("db/migrate/#{file_name}")), file_name) 48 | migration_template "#{file_name}.rb", "db/migrate/#{file_name}.rb" 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/add_data_to_evaluations.rb: -------------------------------------------------------------------------------- 1 | class AddDataToEvaluations < ActiveRecord::Migration 2 | def self.up 3 | add_column :rs_evaluations, :data, :text 4 | end 5 | 6 | def self.down 7 | remove_column :rs_evaluations, :data 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/add_data_to_reputations.rb: -------------------------------------------------------------------------------- 1 | class AddDataToReputations < ActiveRecord::Migration 2 | def self.up 3 | add_column :rs_reputations, :data, :text 4 | end 5 | 6 | def self.down 7 | remove_column :rs_reputations, :data 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/add_evaluations_index.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class AddEvaluationsIndex < ActiveRecord::Migration 18 | def self.up 19 | add_index :rs_evaluations, [:reputation_name, :source_id, :source_type, :target_id, :target_type], :name => "index_rs_evaluations_on_reputation_name_and_source_and_target" 20 | end 21 | 22 | def self.down 23 | remove_index :rs_evaluations, :name => "index_rs_evaluations_on_reputation_name_and_source_and_target" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/add_reputation_messages_index.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class AddReputationMessagesIndex < ActiveRecord::Migration 18 | def self.up 19 | add_index :rs_reputation_messages, [:receiver_id, :sender_id, :sender_type], :name => "index_rs_reputation_messages_on_receiver_id_and_sender" 20 | end 21 | 22 | def self.down 23 | remove_index :rs_reputation_messages, :name => "index_rs_reputation_messages_on_receiver_id_and_sender" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/add_reputations_index.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class AddReputationsIndex < ActiveRecord::Migration 18 | def self.up 19 | add_index :rs_reputations, [:reputation_name, :target_id, :target_type], :name => "index_rs_reputations_on_reputation_name_and_target" 20 | end 21 | 22 | def self.down 23 | remove_index :rs_reputations, :name => "index_rs_reputations_on_reputation_name_and_target" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/change_evaluations_index_to_unique.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class ChangeEvaluationsIndexToUnique < ActiveRecord::Migration 18 | def self.up 19 | remove_index :rs_evaluations, :name => "index_rs_evaluations_on_reputation_name_and_source_and_target" 20 | add_index :rs_evaluations, [:reputation_name, :source_id, :source_type, :target_id, :target_type], :name => "index_rs_evaluations_on_reputation_name_and_source_and_target", :unique => true 21 | end 22 | 23 | def self.down 24 | remove_index :rs_evaluations, :name => "index_rs_evaluations_on_reputation_name_and_source_and_target" 25 | add_index :rs_evaluations, [:reputation_name, :source_id, :source_type, :target_id, :target_type], :name => "index_rs_evaluations_on_reputation_name_and_source_and_target" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/change_reputation_messages_index_to_unique.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class ChangeReputationMessagesIndexToUnique < ActiveRecord::Migration 18 | def self.up 19 | remove_index :rs_reputation_messages, :name => "index_rs_reputation_messages_on_receiver_id_and_sender" 20 | add_index :rs_reputation_messages, [:receiver_id, :sender_id, :sender_type], :name => "index_rs_reputation_messages_on_receiver_id_and_sender", :unique => true 21 | end 22 | 23 | def self.down 24 | remove_index :rs_reputation_messages, :name => "index_rs_reputation_messages_on_receiver_id_and_sender" 25 | add_index :rs_reputation_messages, [:receiver_id, :sender_id, :sender_type], :name => "index_rs_reputation_messages_on_receiver_id_and_sender" 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/change_reputations_index_to_unique.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class ChangeReputationsIndexToUnique < ActiveRecord::Migration 18 | def self.up 19 | remove_index :rs_reputations, :name => "index_rs_reputations_on_reputation_name_and_target" 20 | add_index :rs_reputations, [:reputation_name, :target_id, :target_type], :name => "index_rs_reputations_on_reputation_name_and_target", :unique => true 21 | end 22 | 23 | def self.down 24 | remove_index :rs_reputations, :name => "index_rs_reputations_on_reputation_name_and_target" 25 | add_index :rs_reputations, [:reputation_name, :target_id, :target_type], :name => "index_rs_reputations_on_reputation_name_and_target" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/reputation_system/templates/create_reputation_system.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | class CreateReputationSystem < ActiveRecord::Migration 18 | def self.up 19 | create_table :rs_evaluations do |t| 20 | t.string :reputation_name 21 | t.references :source, :polymorphic => true 22 | t.references :target, :polymorphic => true 23 | t.float :value, :default => 0 24 | t.timestamps 25 | end 26 | 27 | add_index :rs_evaluations, :reputation_name 28 | add_index :rs_evaluations, [:target_id, :target_type] 29 | add_index :rs_evaluations, [:source_id, :source_type] 30 | 31 | create_table :rs_reputations do |t| 32 | t.string :reputation_name 33 | t.float :value, :default => 0 34 | t.string :aggregated_by 35 | t.references :target, :polymorphic => true 36 | t.boolean :active, :default => true 37 | t.timestamps 38 | end 39 | 40 | add_index :rs_reputations, :reputation_name 41 | add_index :rs_reputations, [:target_id, :target_type] 42 | 43 | create_table :rs_reputation_messages do |t| 44 | t.references :sender, :polymorphic => true 45 | t.integer :receiver_id 46 | t.float :weight, :default => 1 47 | t.timestamps 48 | end 49 | 50 | add_index :rs_reputation_messages, [:sender_id, :sender_type] 51 | add_index :rs_reputation_messages, :receiver_id 52 | end 53 | 54 | def self.down 55 | drop_table :rs_evaluations 56 | drop_table :rs_reputations 57 | drop_table :rs_reputation_messages 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/reputation_system.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'reputation_system/base' 18 | require 'reputation_system/query_methods' 19 | require 'reputation_system/finder_methods' 20 | require 'reputation_system/query_builder' 21 | require 'reputation_system/evaluation_methods' 22 | require 'reputation_system/network' 23 | require 'reputation_system/reputation_methods' 24 | require 'reputation_system/scope_methods' 25 | require 'reputation_system/models/evaluation' 26 | require 'reputation_system/models/reputation' 27 | require 'reputation_system/models/reputation_message' 28 | 29 | ActiveRecord::Base.send(:include, ReputationSystem::Base) 30 | -------------------------------------------------------------------------------- /lib/reputation_system/base.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module Base 19 | def self.included(klass) 20 | klass.extend ClassMethods 21 | end 22 | 23 | def get_attributes_of(reputation) 24 | of = reputation[:of] 25 | attrs = (of == :self) ? self : self.instance_eval(of.to_s) if of.is_a?(String) || of.is_a?(Symbol) 26 | attrs = attrs.to_a if attrs.is_a?(ActiveRecord::Associations::CollectionProxy) 27 | attrs = self.instance_exec(self, &of) if of.is_a?(Proc) 28 | attrs = [attrs] unless attrs.is_a? Array 29 | attrs.compact 30 | end 31 | 32 | def evaluate_reputation_scope(scope) 33 | if scope 34 | if self.respond_to? scope 35 | self.send(scope) 36 | else 37 | scope 38 | end 39 | end 40 | end 41 | 42 | module ClassMethods 43 | def has_reputation(reputation_name, options) 44 | has_valid_input = reputation_name && options[:source] 45 | 46 | raise ArgumentError, "has_reputation method received invalid arguments." unless has_valid_input 47 | # Overwrites reputation if the same reputation name is declared in the same model. 48 | # TODO: This should raise exception instead while allowing Rails app to reload in dev mode. 49 | ReputationSystem::Network.remove_reputation_def(name, reputation_name) if has_reputation_for?(reputation_name) 50 | 51 | # If it is first time to be called 52 | unless ancestors.include?(ReputationSystem::ReputationMethods) 53 | has_many :reputations, :as => :target, :class_name => "ReputationSystem::Reputation", :dependent => :destroy do 54 | def for(reputation_name) 55 | self.where(:reputation_name => reputation_name) 56 | end 57 | end 58 | has_many :evaluations, :as => :target, :class_name => "ReputationSystem::Evaluation", :dependent => :destroy do 59 | def for(reputation_name) 60 | self.where(:reputation_name => reputation_name) 61 | end 62 | end 63 | 64 | include ReputationSystem::QueryBuilder 65 | include ReputationSystem::QueryMethods 66 | include ReputationSystem::FinderMethods 67 | include ReputationSystem::ReputationMethods 68 | include ReputationSystem::ScopeMethods 69 | end 70 | 71 | ReputationSystem::Network.add_reputation_def(name, reputation_name, options) 72 | 73 | # evaluation related methods are defined only for primary reputations 74 | include ReputationSystem::EvaluationMethods if ReputationSystem::Network.is_primary_reputation?(name, reputation_name) && !ancestors.include?(ReputationSystem::EvaluationMethods) 75 | end 76 | 77 | def has_reputation_for?(reputation_name) 78 | ReputationSystem::Network.has_reputation_for?(name, reputation_name) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/reputation_system/evaluation_methods.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module EvaluationMethods 19 | module ClassMethods 20 | def evaluated_by(reputation_name, source, *args) 21 | scope = args.first 22 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.name, reputation_name, scope) 23 | source_type = source.class.name 24 | options = {} 25 | options[:select] ||= sanitize_sql_array(["%s.*", self.table_name]) 26 | options[:joins] = sanitize_sql_array(["JOIN rs_evaluations ON %s.id = rs_evaluations.target_id AND rs_evaluations.target_type = ? AND rs_evaluations.reputation_name = ? AND rs_evaluations.source_id = ? AND rs_evaluations.source_type = ?", self.name, srn.to_s, source.id, source_type]) 27 | options[:joins] = sanitize_sql_array([options[:joins], self.table_name]) 28 | joins(options[:joins]).select(options[:select]) 29 | end 30 | end 31 | 32 | def self.included(klass) 33 | klass.extend ClassMethods 34 | end 35 | 36 | def has_evaluation?(reputation_name, source, *args) 37 | scope = args.first 38 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 39 | !!ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(srn, source, self) 40 | end 41 | 42 | def evaluation_by(reputation_name, source, *args) 43 | srn, evaluation = find_srn_and_evaluation(reputation_name, source, args.first) 44 | evaluation ? evaluation.value : nil 45 | end 46 | 47 | def evaluators_for(reputation_name, *args) 48 | scope = args.first 49 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 50 | self.evaluations.for(srn).includes(:source).map(&:source) 51 | end 52 | 53 | def add_evaluation(reputation_name, value, source, *args) 54 | scope = args.first 55 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 56 | process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by] 57 | evaluation = ReputationSystem::Evaluation.create_evaluation(srn, value, source, self) 58 | rep = ReputationSystem::Reputation.find_or_create_reputation(srn, self, process) 59 | ReputationSystem::Reputation.update_reputation_value_with_new_source(rep, evaluation, 1, process) 60 | end 61 | 62 | def update_evaluation(reputation_name, value, source, *args) 63 | srn, evaluation = find_srn_and_evaluation!(reputation_name, source, args.first) 64 | oldValue = evaluation.value 65 | evaluation.value = value 66 | evaluation.save! 67 | process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by] 68 | rep = ReputationSystem::Reputation.find_by_reputation_name_and_target(srn, self) 69 | newSize = rep.received_messages.size 70 | ReputationSystem::Reputation.update_reputation_value_with_updated_source(rep, evaluation, oldValue, newSize, 1, process) 71 | end 72 | 73 | def add_or_update_evaluation(reputation_name, value, source, *args) 74 | srn, evaluation = find_srn_and_evaluation(reputation_name, source, args.first) 75 | if ReputationSystem::Evaluation.exists? :reputation_name => srn, :source_id => source.id, :source_type => source.class.name, :target_id => self.id, :target_type => self.class.name 76 | self.update_evaluation(reputation_name, value, source, *args) 77 | else 78 | self.add_evaluation(reputation_name, value, source, *args) 79 | end 80 | end 81 | 82 | def add_or_delete_evaluation(reputation_name, value, source, *args) 83 | srn, evaluation = find_srn_and_evaluation(reputation_name, source, args.first) 84 | if ReputationSystem::Evaluation.exists? :reputation_name => srn, :source_id => source.id, :source_type => source.class.name, :target_id => self.id, :target_type => self.class.name 85 | !!delete_evaluation_without_validation(srn, evaluation) 86 | else 87 | self.add_evaluation(reputation_name, value, source, *args) 88 | end 89 | end 90 | 91 | def delete_evaluation(reputation_name, source, *args) 92 | srn, evaluation = find_srn_and_evaluation(reputation_name, source, args.first) 93 | if evaluation 94 | !!delete_evaluation_without_validation(srn, evaluation) 95 | else 96 | false 97 | end 98 | end 99 | 100 | def delete_evaluation!(reputation_name, source, *args) 101 | srn, evaluation = find_srn_and_evaluation!(reputation_name, source, args.first) 102 | delete_evaluation_without_validation(srn, evaluation) 103 | end 104 | 105 | def increase_evaluation(reputation_name, value, source, *args) 106 | change_evaluation_value_by(reputation_name, value, source, *args) 107 | end 108 | 109 | def decrease_evaluation(reputation_name, value, source, *args) 110 | change_evaluation_value_by(reputation_name, -value, source, *args) 111 | end 112 | 113 | protected 114 | def find_srn_and_evaluation(reputation_name, source, scope) 115 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 116 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(srn, source, self) 117 | return srn, evaluation 118 | end 119 | 120 | def find_srn_and_evaluation!(reputation_name, source, scope) 121 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 122 | evaluation = find_evaluation!(reputation_name, srn, source) 123 | return srn, evaluation 124 | end 125 | 126 | def find_evaluation!(reputation_name, srn, source) 127 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(srn, source, self) 128 | raise ArgumentError, "Given instance of #{source.class.name} has not evaluated #{reputation_name} of the instance of #{self.class.name} yet." unless evaluation 129 | evaluation 130 | end 131 | 132 | def delete_evaluation_without_validation(srn, evaluation) 133 | process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by] 134 | oldValue = evaluation.value 135 | evaluation.value = process == :product ? 1 : 0 136 | rep = ReputationSystem::Reputation.find_by_reputation_name_and_target(srn, self) 137 | newSize = rep.received_messages.size - 1 138 | ReputationSystem::Reputation.update_reputation_value_with_updated_source(rep, evaluation, oldValue, newSize, 1, process) 139 | evaluation.destroy 140 | end 141 | 142 | def change_evaluation_value_by(reputation_name, value, source, *args) 143 | scope = args.first 144 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 145 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(srn, source, self) 146 | if evaluation.nil? 147 | self.add_evaluation(reputation_name, value, source, scope) 148 | else 149 | new_value = evaluation.value + value 150 | self.update_evaluation(reputation_name, new_value, source, scope) 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/reputation_system/finder_methods.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module FinderMethods 19 | def self.included(klass) 20 | klass.extend ClassMethods 21 | end 22 | 23 | module ClassMethods 24 | 25 | def find_with_reputation(*args) 26 | reputation_name, srn, find_scope, options = parse_query_args(*args) 27 | options[:select] = build_select_statement(table_name, reputation_name, options[:select]) 28 | options[:joins] = build_join_statement(table_name, name, srn, options[:joins]) 29 | options[:conditions] = build_condition_statement(reputation_name, options[:conditions]) 30 | joins(options[:joins]).select(options[:select]).where(options[:conditions]).send(find_scope) 31 | end 32 | 33 | def count_with_reputation(*args) 34 | reputation_name, srn, find_scope, options = parse_query_args(*args) 35 | options[:joins] = build_join_statement(table_name, name, srn, options[:joins]) 36 | options[:conditions] = build_condition_statement(reputation_name, options[:conditions]) 37 | options[:conditions][0].gsub!(reputation_name.to_s, "COALESCE(rs_reputations.value, 0)") 38 | joins(options[:joins]).select(options[:select]).where(options[:conditions]).send(find_scope).count 39 | end 40 | 41 | def find_with_normalized_reputation(*args) 42 | reputation_name, srn, find_scope, options = parse_query_args(*args) 43 | options[:select] = build_select_statement(table_name, reputation_name, options[:select], srn, true) 44 | options[:joins] = build_join_statement(table_name, name, srn, options[:joins]) 45 | options[:conditions] = build_condition_statement(reputation_name, options[:conditions], srn, true) 46 | joins(options[:joins]).select(options[:select]).where(options[:conditions]).send(find_scope) 47 | end 48 | 49 | def find_with_reputation_sql(*args) 50 | reputation_name, srn, find_scope, options = parse_query_args(*args) 51 | options[:select] = build_select_statement(table_name, reputation_name, options[:select]) 52 | options[:joins] = build_join_statement(table_name, name, srn, options[:joins]) 53 | options[:conditions] = build_condition_statement(reputation_name, options[:conditions]) 54 | if respond_to?(:construct_finder_sql, true) 55 | construct_finder_sql(options) 56 | elsif respond_to?(:construct_finder_arel, true) 57 | construct_finder_arel(options).to_sql 58 | else 59 | joins(options[:joins]).select(options[:select]).where(options[:conditions]).send(find_scope).to_sql 60 | end 61 | end 62 | 63 | protected 64 | 65 | def parse_query_args(*args) 66 | case args.length 67 | when 2 68 | find_scope = args[1] 69 | options = {} 70 | when 3 71 | find_scope = args[1] 72 | options = args[2] 73 | when 4 74 | scope = args[1] 75 | find_scope = args[2] 76 | options = args[3] 77 | else 78 | raise ArgumentError, "Expecting 2, 3 or 4 arguments but got #{args.length}" 79 | end 80 | reputation_name = args[0] 81 | srn = ReputationSystem::Network.get_scoped_reputation_name(name, reputation_name, scope) 82 | [reputation_name, srn, find_scope, options] 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/reputation_system/models/evaluation.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | class Evaluation < ActiveRecord::Base 19 | self.table_name = 'rs_evaluations' 20 | 21 | belongs_to :source, :polymorphic => true 22 | belongs_to :target, :polymorphic => true 23 | has_one :sent_messages, :as => :sender, :class_name => 'ReputationSystem::ReputationMessage', :dependent => :destroy 24 | 25 | # Sets an appropriate source type in case of Single Table Inheritance. 26 | before_validation :set_source_type_for_sti 27 | 28 | # the same source cannot evaluate the same target more than once. 29 | validates_uniqueness_of :source_id, :scope => [:reputation_name, :source_type, :target_id, :target_type] 30 | validate :source_must_be_defined_for_reputation_in_network 31 | 32 | serialize :data, Hash 33 | 34 | def self.find_by_reputation_name_and_source_and_target(reputation_name, source, target) 35 | source_type = get_source_type_for_sti(source.class.name, target.class.name, reputation_name) 36 | ReputationSystem::Evaluation.where( 37 | :reputation_name => reputation_name.to_s, 38 | :source_id => source.id, 39 | :source_type => source_type, 40 | :target_id => target.id, 41 | :target_type => target.class.name 42 | ).first 43 | end 44 | 45 | def self.create_evaluation(reputation_name, value, source, target) 46 | ReputationSystem::Evaluation.create!(:reputation_name => reputation_name.to_s, :value => value, 47 | :source_id => source.id, :source_type => source.class.name, 48 | :target_id => target.id, :target_type => target.class.name) 49 | end 50 | 51 | # Override exists? class method. 52 | class << self 53 | alias :original_exists? :exists? 54 | 55 | def exists?(options={}) 56 | if options[:source_type] && options[:target_type] && options[:reputation_name] 57 | options[:source_type] = get_source_type_for_sti(options[:source_type], options[:target_type], options[:reputation_name]) 58 | end 59 | original_exists? options 60 | end 61 | end 62 | 63 | protected 64 | 65 | def self.get_source_type_for_sti(source_type, target_type, reputation_name) 66 | valid_source_type = ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize 67 | source_class = source_type.constantize 68 | while source_class && valid_source_type != source_class.name && source_class.name != "ActiveRecord::Base" 69 | source_class = source_class.superclass 70 | end 71 | source_class ? source_class.name : nil 72 | end 73 | 74 | def set_source_type_for_sti 75 | sti_source_type = self.class.get_source_type_for_sti(source_type, target_type, reputation_name) 76 | self.source_type = sti_source_type if sti_source_type 77 | end 78 | 79 | def source_must_be_defined_for_reputation_in_network 80 | unless source_type == ReputationSystem::Network.get_reputation_def(target_type, reputation_name)[:source].to_s.camelize 81 | errors.add(:source_type, "#{source_type} is not source of #{reputation_name} reputation") 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/reputation_system/models/reputation.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | class Reputation < ActiveRecord::Base 19 | self.table_name = 'rs_reputations' 20 | 21 | belongs_to :target, :polymorphic => true 22 | has_many :received_messages, :class_name => 'ReputationSystem::ReputationMessage', :foreign_key => :receiver_id, :dependent => :destroy do 23 | def from(sender) 24 | self.find_by_sender_id_and_sender_type(sender.id, sender.class.to_s) 25 | end 26 | end 27 | has_many :sent_messages, :as => :sender, :class_name => 'ReputationSystem::ReputationMessage', :dependent => :destroy 28 | 29 | before_validation :set_target_type_for_sti 30 | before_save :change_zero_value_in_case_of_product_process 31 | 32 | validates_uniqueness_of :reputation_name, :scope => [:target_id, :target_type] 33 | 34 | serialize :data, Hash 35 | 36 | def self.find_by_reputation_name_and_target(reputation_name, target) 37 | target_type = get_target_type_for_sti(target, reputation_name) 38 | ReputationSystem::Reputation.find_by_reputation_name_and_target_id_and_target_type(reputation_name.to_s, target.id, target_type) 39 | end 40 | 41 | # All external access to reputation should use this since they are created lazily. 42 | def self.find_or_create_reputation(reputation_name, target, process) 43 | rep = find_by_reputation_name_and_target(reputation_name, target) 44 | rep ? rep : create_reputation(reputation_name, target, process) 45 | end 46 | 47 | def self.create_reputation(reputation_name, target, process) 48 | create_options = {:reputation_name => reputation_name.to_s, :target_id => target.id, 49 | :target_type => target.class.name, :aggregated_by => process.to_s} 50 | rep = create(create_options) 51 | initialize_reputation_value(rep, target, process) 52 | end 53 | 54 | def self.update_reputation_value_with_new_source(rep, source, weight, process) 55 | weight ||= 1 # weight is 1 by default. 56 | size = rep.received_messages.size 57 | valueBeforeUpdate = size > 0 ? rep.value : nil 58 | newValue = source.value 59 | case process.to_sym 60 | when :sum 61 | rep.value += (newValue * weight) 62 | when :average 63 | rep.value = (rep.value * size + newValue * weight) / (size + 1) 64 | when :product 65 | rep.value *= (newValue * weight) 66 | else 67 | if source.target.respond_to?(process) 68 | rep.value = source.target.send(process, rep, source, weight) 69 | else 70 | raise ArgumentError, "#{process} process is not supported yet" 71 | end 72 | end 73 | save_succeeded = rep.save 74 | ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, rep) 75 | propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target 76 | save_succeeded 77 | end 78 | 79 | def self.update_reputation_value_with_updated_source(rep, source, oldValue, newSize, weight, process) 80 | weight ||= 1 # weight is 1 by default. 81 | oldSize = rep.received_messages.size 82 | valueBeforeUpdate = oldSize > 0 ? rep.value : nil 83 | newValue = source.value 84 | if newSize == 0 85 | rep.value = process.to_sym == :product ? 1 : 0 86 | else 87 | case process.to_sym 88 | when :sum 89 | rep.value += (newValue - oldValue) * weight 90 | when :average 91 | rep.value = (rep.value * oldSize + (newValue - oldValue) * weight) / newSize 92 | when :product 93 | rep.value = (rep.value * newValue) / oldValue 94 | else 95 | if source.target.respond_to?(process) 96 | rep.value = source.target.send(process, rep, source, weight, oldValue, newSize) 97 | else 98 | raise ArgumentError, "#{process} process is not supported yet" 99 | end 100 | end 101 | end 102 | save_succeeded = rep.save 103 | propagate_updated_reputation_value(rep, valueBeforeUpdate) if rep.target 104 | save_succeeded 105 | end 106 | 107 | def normalized_value 108 | if self.active == 1 || self.active == true 109 | max = ReputationSystem::Reputation.max(self.reputation_name, self.target_type) 110 | min = ReputationSystem::Reputation.min(self.reputation_name, self.target_type) 111 | if max && min 112 | range = max - min 113 | range == 0 ? 0 : (self.value - min) / range 114 | else 115 | 0 116 | end 117 | else 118 | 0 119 | end 120 | end 121 | 122 | protected 123 | 124 | # Updates reputation value for new reputation if its source already exist. 125 | def self.initialize_reputation_value(receiver, target, process) 126 | name = receiver.reputation_name 127 | unless ReputationSystem::Network.is_primary_reputation?(target.class.name, name) 128 | sender_defs = ReputationSystem::Network.get_reputation_def(target.class.name, name)[:source] 129 | sender_defs.each do |sd| 130 | sender_targets = target.get_attributes_of(sd) 131 | sender_targets.each do |st| 132 | update_reputation_if_source_exist(sd, st, receiver, process) if receiver.target 133 | end 134 | end 135 | end 136 | receiver 137 | end 138 | 139 | # Propagates updated reputation value to the reputations whose source is the updated reputation. 140 | def self.propagate_updated_reputation_value(sender, oldValue) 141 | receiver_defs = ReputationSystem::Network.get_reputation_def(sender.target.class.name, sender.reputation_name)[:source_of] 142 | receiver_defs.each do |rd| 143 | targets = sender.target.get_attributes_of(rd) 144 | targets.each do |target| 145 | scope = sender.target.evaluate_reputation_scope(rd[:scope]) 146 | send_reputation_message_to_receiver(rd[:reputation], sender, target, scope, oldValue) 147 | end 148 | end if receiver_defs 149 | end 150 | 151 | def self.send_reputation_message_to_receiver(reputation_name, sender, target, scope, oldValue) 152 | srn = ReputationSystem::Network.get_scoped_reputation_name(target.class.name, reputation_name, scope) 153 | process = ReputationSystem::Network.get_reputation_def(target.class.name, srn)[:aggregated_by] 154 | receiver = find_by_reputation_name_and_target(srn, target) 155 | if receiver 156 | weight = ReputationSystem::Network.get_weight_of_source_from_reputation_name_of_target(target, sender.reputation_name, srn) 157 | update_reputation_value(receiver, sender, weight, process, oldValue) 158 | # If r is new then value update will be done when it is initialized. 159 | else 160 | create_reputation(srn, target, process) 161 | end 162 | end 163 | 164 | def self.update_reputation_value(receiver, sender, weight, process, oldValue) 165 | unless oldValue 166 | update_reputation_value_with_new_source(receiver, sender, weight, process) 167 | else 168 | newSize = receiver.received_messages.size 169 | update_reputation_value_with_updated_source(receiver, sender, oldValue, newSize, weight, process) 170 | end 171 | end 172 | 173 | def self.update_reputation_if_source_exist(sd, st, receiver, process) 174 | scope = receiver.target.evaluate_reputation_scope(sd[:scope]) 175 | srn = ReputationSystem::Network.get_scoped_reputation_name(st.class.name, sd[:reputation], scope) 176 | source = find_by_reputation_name_and_target(srn, st) 177 | if source 178 | update_reputation_value_with_new_source(receiver, source, sd[:weight], process) 179 | ReputationSystem::ReputationMessage.add_reputation_message_if_not_exist(source, receiver) 180 | end 181 | end 182 | 183 | def self.max(reputation_name, target_type) 184 | ReputationSystem::Reputation.where(:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true).maximum(:value) 185 | end 186 | 187 | def self.min(reputation_name, target_type) 188 | ReputationSystem::Reputation.where(:reputation_name => reputation_name.to_s, :target_type => target_type, :active => true).minimum(:value) 189 | end 190 | 191 | def self.get_target_type_for_sti(target, reputation_name) 192 | target_class = target.class 193 | defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym] 194 | while target_class && target_class.name != "ActiveRecord::Base" && defs && defs.empty? 195 | target_class = target_class.superclass 196 | defs = ReputationSystem::Network.get_reputation_defs(target_class.name)[reputation_name.to_sym] 197 | end 198 | target_class ? target_class.name : nil 199 | end 200 | 201 | def set_target_type_for_sti 202 | sti_target_type = self.class.get_target_type_for_sti(target, reputation_name) 203 | self.target_type = sti_target_type if sti_target_type 204 | end 205 | 206 | def change_zero_value_in_case_of_product_process 207 | self.value = 1 if self.value == 0 && self.aggregated_by == "product" 208 | end 209 | 210 | def remove_associated_messages 211 | ReputationSystem::ReputationMessage.delete_all(:sender_type => self.class.name, :sender_id => self.id) 212 | ReputationSystem::ReputationMessage.delete_all(:receiver_id => self.id) 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/reputation_system/models/reputation_message.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | class ReputationMessage < ActiveRecord::Base 19 | self.table_name = 'rs_reputation_messages' 20 | belongs_to :sender, :polymorphic => true 21 | belongs_to :receiver, :class_name => 'ReputationSystem::Reputation' 22 | 23 | # The same sender cannot send massage to the same receiver more than once. 24 | validates_uniqueness_of :receiver_id, :scope => [:sender_id, :sender_type] 25 | validate :sender_must_be_evaluation_or_reputation 26 | 27 | after_destroy :delete_sender_if_evaluation 28 | 29 | def self.add_reputation_message_if_not_exist(sender, receiver) 30 | rm = create(:sender => sender, :receiver => receiver) 31 | receiver.received_messages.push rm if rm.valid? 32 | end 33 | 34 | protected 35 | 36 | def delete_sender_if_evaluation 37 | sender.destroy if sender.is_a?(ReputationSystem::Evaluation) 38 | end 39 | 40 | def sender_must_be_evaluation_or_reputation 41 | unless sender.is_a?(ReputationSystem::Evaluation) || sender.is_a?(ReputationSystem::Reputation) 42 | errors.add(:sender, "must be an evaluation or a reputation") 43 | end 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/reputation_system/network.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | class Network 19 | class << self 20 | def has_reputation_for?(class_name, reputation_name) 21 | reputation_def = get_reputation_def(class_name, reputation_name) 22 | !!reputation_def[:source] 23 | end 24 | 25 | def get_reputation_defs(class_name) 26 | network[class_name.to_sym] ||= {} 27 | end 28 | 29 | def get_reputation_def(class_name, reputation_name) 30 | reputation_def = {} 31 | unless class_name == "ActiveRecord::Base" 32 | reputation_defs = get_reputation_defs(class_name) 33 | reputation_defs[reputation_name.to_sym] ||= {} 34 | reputation_def = reputation_defs[reputation_name.to_sym] 35 | if reputation_def == {} 36 | begin 37 | # This recursion finds reputation definition in the ancestor in case of STI. 38 | klass = class_name.constantize.superclass 39 | reputation_def = get_reputation_def(klass.name, reputation_name) if klass 40 | rescue NameError 41 | # Class might have not been initialized yet at this point. 42 | end 43 | end 44 | end 45 | reputation_def 46 | end 47 | 48 | def add_reputation_def(class_name, reputation_name, options) 49 | reputation_defs = get_reputation_defs(class_name) 50 | options[:source] = convert_to_array_if_hash(options[:source]) 51 | options[:source_of] ||= [] 52 | options[:source_of] = convert_to_array_if_hash(options[:source_of]) 53 | options[:aggregated_by] = options[:aggregated_by] || :sum 54 | assign_self_as_default_value_for_of_attr(options[:source]) 55 | assign_self_as_default_value_for_of_attr(options[:source_of]) 56 | reputation_defs[reputation_name] = options 57 | options[:source].each do |s| 58 | src_class_name = derive_class_name_from_attribute(class_name, s[:of]) 59 | if has_reputation_for?(src_class_name, s[:reputation]) 60 | derive_source_of_from_source(class_name, reputation_name, s, src_class_name) 61 | else 62 | # Because the source class might not have been initialized at this time. 63 | derive_source_of_from_source_later(class_name, reputation_name, s, src_class_name) 64 | end 65 | end unless is_primary_reputation?(class_name, reputation_name) 66 | perform_derive_later(class_name, reputation_name) 67 | construct_scoped_reputation_options(class_name, reputation_name, options) 68 | end 69 | 70 | def remove_reputation_def(class_name, reputation_name) 71 | reputation_defs = get_reputation_defs(class_name) 72 | reputation_defs.delete(reputation_name.to_sym) 73 | end 74 | 75 | def is_primary_reputation?(class_name, reputation_name) 76 | options = get_reputation_def(class_name, reputation_name) 77 | options[:source].is_a?(Symbol) 78 | end 79 | 80 | def add_scope_for(class_name, reputation_name, scope) 81 | options = get_reputation_def(class_name, reputation_name) 82 | if has_scope?(class_name, reputation_name, scope) 83 | raise ArgumentError, "#{scope} is already defined for #{reputation_name}" 84 | else 85 | options[:scopes].push scope.to_sym if options[:scopes] 86 | create_scoped_reputation_def(class_name, reputation_name, scope, options) 87 | end 88 | end 89 | 90 | def has_scopes?(class_name, reputation_name) 91 | !get_reputation_def(class_name, reputation_name)[:scopes].nil? 92 | end 93 | 94 | def has_scope?(class_name, reputation_name, scope) 95 | scopes = get_reputation_def(class_name, reputation_name)[:scopes] 96 | scopes && scopes.include?(scope.to_sym) 97 | end 98 | 99 | def get_scoped_reputation_name(class_name, reputation_name, scope) 100 | raise ArgumentError, "#{reputation_name.to_s} is not defined for #{class_name}" unless has_reputation_for?(class_name, reputation_name) 101 | scope = scope.to_sym if scope 102 | validate_scope_necessity(class_name, reputation_name, scope) 103 | validate_scope_existence(class_name, reputation_name, scope) 104 | "#{reputation_name}#{"_#{scope}" if scope}" 105 | end 106 | 107 | def get_weight_of_source_from_reputation_name_of_target(target, source_name, reputation_name) 108 | source = get_reputation_def(target.class.name, reputation_name)[:source] 109 | if source.is_a?(Array) 110 | source.each do |s| 111 | srn = get_scoped_reputation_name_from_source_def_and_target(s, target) 112 | return s[:weight] if srn.to_sym == source_name.to_sym 113 | end 114 | else 115 | source[:weight] 116 | end 117 | end 118 | 119 | protected 120 | 121 | def network 122 | @network ||= {} 123 | end 124 | 125 | def data_for_derive_later 126 | @data_for_derive_later ||= {} 127 | end 128 | 129 | def create_scoped_reputation_def(class_name, reputation_name, scope, options) 130 | raise ArgumentError, "#{reputation_name} does not have scope." unless has_scopes?(class_name, reputation_name) 131 | scope_options = options.reject { |k, v| ![:source, :aggregated_by].include? k } 132 | reputation_def = get_reputation_def(class_name, reputation_name) 133 | unless is_primary_reputation?(class_name, reputation_name) 134 | scope_options[:source] = [] 135 | reputation_def[:source].each { |sd| scope_options[:source].push create_source_reputation_def(sd, scope) } 136 | end 137 | (reputation_def[:source_of] || []).each do |so| 138 | if source_of_defined_for_scope?(so, scope) 139 | scope_options[:source_of] ||= [] 140 | scope_options[:source_of].push so 141 | end 142 | end 143 | srn = get_scoped_reputation_name(class_name, reputation_name, scope) 144 | network[class_name.to_sym][srn.to_sym] = scope_options 145 | end 146 | 147 | def create_source_reputation_def(source_def, scope) 148 | rep = {} 149 | rep[:reputation] = source_def[:reputation] 150 | # Passing "this" is not pretty but in some case "instance_exec" method 151 | # does not give right context for some reason. 152 | # This could be ruby bug. Needs further investigation. 153 | if source_def[:of].is_a? Proc 154 | rep[:of] = lambda { |this| instance_exec(this, scope.to_s, &source_def[:of]) } 155 | else 156 | rep[:of] = source_def[:of] 157 | end 158 | rep 159 | end 160 | 161 | def get_scoped_reputation_name_from_source_def_and_target(source_def, target) 162 | scope = target.evaluate_reputation_scope(source_def[:scope]) if source_def[:scope] 163 | of = target.get_attributes_of(source_def) 164 | class_name = (of.is_a?(Array) ? of[0] : of).class.name 165 | get_scoped_reputation_name(class_name, source_def[:reputation], scope) 166 | end 167 | 168 | def source_of_defined_for_scope?(source_of_def, scope) 169 | defined_for_scope = source_of_def[:defined_for_scope] 170 | defined_for_scope.nil? || (defined_for_scope && defined_for_scope.include?(scope.to_sym)) 171 | end 172 | 173 | def construct_scoped_reputation_options(class_name, reputation_name, options) 174 | scopes = get_reputation_def(class_name, reputation_name)[:scopes] 175 | scopes.each do |scope| 176 | create_scoped_reputation_def(class_name, reputation_name, scope, options) 177 | end if scopes 178 | end 179 | 180 | def derive_source_of_from_source(class_name, reputation_name, source, src_class_name) 181 | of_value = derive_of_value(class_name, source[:of], src_class_name) 182 | reputation_def = get_reputation_def(src_class_name, source[:reputation]) 183 | reputation_def[:source_of] ||= [] 184 | unless source_of_include_reputation?(reputation_def[:source_of], reputation_name) 185 | reputation_def[:source_of] << {:reputation => reputation_name.to_sym, :of => of_value.to_sym} 186 | end 187 | end 188 | 189 | def derive_of_value(class_name, source_of, src_class_name) 190 | if not_source_of_self?(source_of) 191 | attr = class_name.tableize 192 | class_has_attribute?(src_class_name, attr) ? attr : attr.chomp('s') 193 | else 194 | "self" 195 | end 196 | end 197 | 198 | def not_source_of_self?(source_of) 199 | source_of && source_of.is_a?(Symbol) && source_of != :self 200 | end 201 | 202 | def class_has_attribute?(class_name, attribute) 203 | klass = class_name.to_s.constantize 204 | klass.instance_methods.include?(attribute.to_s) || klass.instance_methods.include?(attribute.to_sym) 205 | end 206 | 207 | def source_of_include_reputation?(source_of, reputation_name) 208 | source_of.map { |rep| rep[:reputation] }.include?(reputation_name.to_sym) 209 | end 210 | 211 | def derive_source_of_from_source_later(class_name, reputation_name, source, src_class_name) 212 | reputation = source[:reputation].to_sym 213 | src_class_name = src_class_name.to_sym 214 | data = data_for_derive_later 215 | data[src_class_name] ||= {} 216 | data[src_class_name][reputation] ||= {} 217 | data[src_class_name][reputation].merge!(:source => source, :class_name => class_name, :reputation_name => reputation_name) 218 | end 219 | 220 | def perform_derive_later(src_class_name, reputation) 221 | src_class_name = src_class_name.to_sym 222 | reputation = reputation.to_sym 223 | data = data_for_derive_later 224 | if data[src_class_name] && data[src_class_name][reputation] 225 | class_name = data[src_class_name][reputation][:class_name] 226 | source = data[src_class_name][reputation][:source] 227 | reputation_name = data[src_class_name][reputation][:reputation_name] 228 | derive_source_of_from_source(class_name, reputation_name, source, src_class_name) 229 | data[src_class_name].delete(reputation) 230 | end 231 | end 232 | 233 | def derive_class_name_from_attribute(class_name, attribute) 234 | if attribute && attribute != :self && attribute != "self" 235 | attribute.to_s.camelize.chomp('s') 236 | else 237 | class_name 238 | end 239 | end 240 | 241 | def convert_to_array_if_hash(tar) 242 | tar.is_a?(Hash) ? [tar] : tar 243 | end 244 | 245 | def assign_self_as_default_value_for_of_attr(tar) 246 | tar.each { |s| s[:of] = :self unless s[:of] } if tar.is_a? Array 247 | end 248 | 249 | def validate_scope_necessity(class_name, reputation_name, scope) 250 | if scope.nil? && has_scopes?(class_name, reputation_name) 251 | raise ArgumentError, "Evaluations of #{reputation_name} must have scope specified." 252 | end 253 | end 254 | 255 | def validate_scope_existence(class_name, reputation_name, scope) 256 | if !scope.nil? && !has_scope?(class_name, reputation_name, scope) 257 | raise ArgumentError, "#{reputation_name} does not have scope #{scope}" 258 | end 259 | end 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /lib/reputation_system/query_builder.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module QueryBuilder 19 | def self.included(klass) 20 | klass.extend ClassMethods 21 | end 22 | 23 | module ClassMethods 24 | DELTA = 0.000001 25 | REPUTATION_JOIN_STATEMENT = "LEFT JOIN rs_reputations ON %s.id = rs_reputations.target_id AND rs_reputations.target_type = ? AND rs_reputations.reputation_name = ? AND rs_reputations.active = ?" 26 | REPUTATION_FIELD_STRING = "COALESCE(rs_reputations.value, 0)" 27 | 28 | def build_select_statement(table_name, reputation_name, select=nil, srn=nil, normalize=false) 29 | select = sanitize_sql_array(["%s.*", table_name]) unless select 30 | if normalize 31 | sanitize_sql_array(["%s, %s AS normalized_%s", select, normalized_field_string(srn), reputation_name.to_s]) 32 | else 33 | sanitize_sql_array(["%s, %s AS %s", select, REPUTATION_FIELD_STRING, reputation_name.to_s]) 34 | end 35 | end 36 | 37 | def build_select_statement_with_reputation_only(table_name, reputation_name, srn=nil, normalize=false) 38 | if normalize 39 | sanitize_sql_array(["%s AS normalized_%s", normalized_field_string(srn), reputation_name.to_s]) 40 | else 41 | sanitize_sql_array(["%s AS %s", REPUTATION_FIELD_STRING, reputation_name.to_s]) 42 | end 43 | end 44 | 45 | def build_condition_statement(reputation_name, conditions=nil, srn=nil, normalize=false) 46 | conditions ||= [""] 47 | conditions = [conditions] unless conditions.is_a? Array 48 | if normalize 49 | normalized_reputation_name = sanitize_sql_array(["normalized_%s", reputation_name.to_s]) 50 | conditions[0] = conditions[0].gsub(normalized_reputation_name, normalized_field_string(srn)) 51 | end 52 | conditions[0] = conditions[0].gsub(reputation_name.to_s, REPUTATION_FIELD_STRING) 53 | conditions 54 | end 55 | 56 | def build_join_statement(table_name, class_name, srn, joins=nil) 57 | joins ||= [] 58 | joins = [joins] unless joins.is_a? Array 59 | rep_join = sanitize_sql_array([REPUTATION_JOIN_STATEMENT, class_name.to_s, srn.to_s, true]) 60 | rep_join = sanitize_sql_array([rep_join, table_name]) 61 | joins << rep_join 62 | end 63 | 64 | protected 65 | 66 | def normalized_field_string(srn) 67 | max = ReputationSystem::Reputation.max(srn, self.name) 68 | min = ReputationSystem::Reputation.min(srn, self.name) 69 | range = max - min 70 | if range < DELTA 71 | "(0)" 72 | else 73 | sanitize_sql_array(["((rs_reputations.value - %s) / %s)", min, range]) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/reputation_system/query_methods.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module QueryMethods 19 | def self.included(klass) 20 | klass.extend ClassMethods 21 | end 22 | 23 | module ClassMethods 24 | def with_reputation(*args) 25 | warn "[DEPRECATION] `with_reputation` will be deprecated in version 3.0.0. Please use finder methods instead." 26 | reputation_name, srn = parse_arel_query_args(args) 27 | select = build_select_statement(table_name, reputation_name) 28 | joins = build_join_statement(table_name, name, srn) 29 | self.select(select).joins(joins) 30 | end 31 | 32 | def with_reputation_only(*args) 33 | warn "[DEPRECATION] `with_reputation_only` will be deprecated in version 3.0.0. Please use finder methods instead." 34 | reputation_name, srn = parse_arel_query_args(args) 35 | select = build_select_statement_with_reputation_only(table_name, reputation_name) 36 | joins = build_join_statement(table_name, name, srn) 37 | self.select(select).joins(joins) 38 | end 39 | 40 | def with_normalized_reputation(*args) 41 | warn "[DEPRECATION] `with_normalized_reputation` will be deprecated in version 3.0.0. Please use finder methods instead." 42 | reputation_name, srn = parse_arel_query_args(args) 43 | select = build_select_statement(table_name, reputation_name, nil, srn, true) 44 | joins = build_join_statement(table_name, name, srn) 45 | self.select(select).joins(joins) 46 | end 47 | 48 | def with_normalized_reputation_only(*args) 49 | warn "[DEPRECATION] `with_normalized_reputation_only` will be deprecated in version 3.0.0. Please use finder methods instead." 50 | reputation_name, srn = parse_arel_query_args(args) 51 | select = build_select_statement_with_reputation_only(table_name, reputation_name, srn, true) 52 | joins = build_join_statement(table_name, name, srn) 53 | self.select(select).joins(joins) 54 | end 55 | 56 | protected 57 | 58 | def parse_arel_query_args(args) 59 | reputation_name = args[0] 60 | srn = ReputationSystem::Network.get_scoped_reputation_name(name, reputation_name, args[1]) 61 | [reputation_name, srn] 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/reputation_system/reputation_methods.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module ReputationMethods 19 | def reputation_for(reputation_name, *args) 20 | find_reputation(reputation_name, args.first).value 21 | end 22 | 23 | def normalized_reputation_for(reputation_name, *args) 24 | find_reputation(reputation_name, args.first).normalized_value 25 | end 26 | 27 | def activate_all_reputations 28 | ReputationSystem::Reputation.where(:target_id => self.id, :target_type => self.class.name, :active => false).each do |r| 29 | r.active = true 30 | r.save! 31 | end 32 | end 33 | 34 | def deactivate_all_reputations 35 | ReputationSystem::Reputation.where(:target_id => self.id, :target_type => self.class.name, :active => true).each do |r| 36 | r.active = false 37 | r.save! 38 | end 39 | end 40 | 41 | def reputations_activated?(reputation_name) 42 | r = ReputationSystem::Reputation.where(:reputation_name => reputation_name.to_s, :target_id => self.id, :target_type => self.class.name).first 43 | r ? r.active : false 44 | end 45 | 46 | def rank_for(reputation_name, *args) 47 | scope = args.first 48 | my_value = self.reputation_for(reputation_name, scope) 49 | self.class.count_with_reputation(reputation_name, scope, :all, 50 | :conditions => ["rs_reputations.value > ?", my_value] 51 | ) + 1 52 | end 53 | 54 | protected 55 | def find_reputation(reputation_name, scope) 56 | raise ArgumentError, "#{reputation_name} is not valid" if !self.class.has_reputation_for?(reputation_name) 57 | srn = ReputationSystem::Network.get_scoped_reputation_name(self.class.name, reputation_name, scope) 58 | process = ReputationSystem::Network.get_reputation_def(self.class.name, srn)[:aggregated_by] 59 | ReputationSystem::Reputation.find_or_create_reputation(srn, self, process) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/reputation_system/scope_methods.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | module ScopeMethods 19 | def self.included(klass) 20 | klass.extend ClassMethods 21 | end 22 | 23 | module ClassMethods 24 | def add_scope_for(reputation_name, scope) 25 | ReputationSystem::Network.add_scope_for(name, reputation_name, scope) 26 | end 27 | 28 | def has_scopes?(reputation_name) 29 | ReputationSystem::Network.has_scopes?(name, reputation_name, scope) 30 | end 31 | 32 | def has_scope?(reputation_name, scope) 33 | ReputationSystem::Network.has_scope?(name, reputation_name, scope) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/reputation_system/version.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | module ReputationSystem 18 | VERSION = "3.0.1" 19 | end 20 | -------------------------------------------------------------------------------- /spec/reputation_system/base_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::Base do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | context "Mixin" do 29 | describe "#has_reputation" do 30 | it "should add 'add_evaluation' method to a model with primary reputation" do 31 | expect(@question.respond_to?(:add_evaluation)).to eq(true) 32 | expect(@answer.respond_to?(:add_evaluation)).to eq(true) 33 | end 34 | 35 | it "should not add 'add_evaluation' method to a model without primary reputation" do 36 | expect(@user.respond_to?(:add_evaluation)).to eq(false) 37 | end 38 | 39 | it "should add 'reputation_for' method to a model with reputation" do 40 | expect(@user.respond_to?(:reputation_for)).to eq(true) 41 | expect(@question.respond_to?(:reputation_for)).to eq(true) 42 | end 43 | 44 | it "should add 'normalized_reputation_for' method to a model with reputation" do 45 | expect(@user.respond_to?(:normalized_reputation_for)).to eq(true) 46 | expect(@question.respond_to?(:normalized_reputation_for)).to eq(true) 47 | end 48 | 49 | it "should delete reputations if target is deleted" do 50 | @question.add_evaluation(:total_votes, 5, @user) 51 | reputation_count = ReputationSystem::Reputation.count 52 | message_count = ReputationSystem::ReputationMessage.count 53 | @question.destroy 54 | expect(ReputationSystem::Reputation.count).to be < reputation_count 55 | expect(ReputationSystem::ReputationMessage.count).to be < message_count 56 | end 57 | end 58 | end 59 | 60 | context "Association" do 61 | describe "#reputations" do 62 | it "should define reputations association" do 63 | expect(@question.respond_to?(:reputations)).to eq(true) 64 | end 65 | it "should return all reputations for the target" do 66 | @question.add_evaluation(:total_votes, 2, @user) 67 | @question.add_evaluation(:difficulty, 2, @user) 68 | expect(@question.reputations.count).to eq(2) 69 | end 70 | describe "#for" do 71 | it "should return empty array if there is no reputation for the target" do 72 | expect(@question.reputations.for(:total_votes)).to eq([]) 73 | end 74 | it "should return all reputations of the given type for the target" do 75 | @question.add_evaluation(:total_votes, 2, @user) 76 | @question.add_evaluation(:difficulty, 2, @user) 77 | expect(@question.reputations.for(:total_votes).count).to eq(1) 78 | end 79 | end 80 | end 81 | 82 | describe "#evaluations" do 83 | it "should define evaluations association" do 84 | expect(@question.respond_to?(:evaluations)).to eq(true) 85 | end 86 | it "should return all evaluations for the target" do 87 | @question.add_evaluation(:total_votes, 2, @user) 88 | @question.add_evaluation(:difficulty, 2, @user) 89 | expect(@question.evaluations.count).to eq(2) 90 | end 91 | describe "#for" do 92 | it "should return empty array if there is no evaluation for the target" do 93 | expect(@question.evaluations.for(:total_votes)).to eq([]) 94 | end 95 | it "should return all evaluations of the given type for the target" do 96 | @question.add_evaluation(:total_votes, 2, @user) 97 | @question.add_evaluation(:difficulty, 2, @user) 98 | expect(@question.evaluations.for(:total_votes).count).to eq(1) 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/reputation_system/evaluation_methods_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::EvaluationMethods do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | context "Primary Reputation" do 29 | describe "#has_evaluation?" do 30 | it "should return false if it has not already been evaluated by a given source" do 31 | user = User.create! :name => 'katsuya' 32 | @question.add_evaluation(:total_votes, 3, user) 33 | expect(@question.has_evaluation?(:total_votes, @user)).to be false 34 | end 35 | it "should return true if it has been evaluated by a given source" do 36 | @question.add_evaluation(:total_votes, 3, @user) 37 | expect(@question.has_evaluation?(:total_votes, @user)).to be true 38 | end 39 | context "With Scopes" do 40 | it "should return false if it has not already been evaluated by a given source" do 41 | @phrase.add_evaluation(:difficulty_with_scope, 3, @user, :s1) 42 | expect(@phrase.has_evaluation?(:difficulty_with_scope, @user, :s2)).to be false 43 | end 44 | it "should return true if it has been evaluated by a given source" do 45 | @phrase.add_evaluation(:difficulty_with_scope, 3, @user, :s1) 46 | expect(@phrase.has_evaluation?(:difficulty_with_scope, @user, :s1)).to be true 47 | end 48 | end 49 | end 50 | 51 | describe "#evaluated_by" do 52 | it "should return an empty array if it is not evaluated by a given source" do 53 | expect(Question.evaluated_by(:total_votes, @user)).to eq([]) 54 | end 55 | 56 | it "should return an array of targets evaluated by a given source" do 57 | user2 = User.create!(:name => 'katsuya') 58 | question2 = Question.create!(:text => 'Question 2', :author_id => @user.id) 59 | question3 = Question.create!(:text => 'Question 3', :author_id => @user.id) 60 | expect(@question.add_evaluation(:total_votes, 1, @user)).to be true 61 | expect(question2.add_evaluation(:total_votes, 2, user2)).to be true 62 | expect(question3.add_evaluation(:total_votes, 3, @user)).to be true 63 | expect(Question.evaluated_by(:total_votes, @user)).to eq([@question, question3]) 64 | expect(Question.evaluated_by(:total_votes, user2)).to eq([question2]) 65 | end 66 | 67 | context "With Scopes" do 68 | it "should return an array of targets evaluated by a given source on appropriate scope" do 69 | user2 = User.create!(:name => 'katsuya') 70 | phrase2 = Phrase.create!(:text => "Two") 71 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 72 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 73 | expect(@phrase.add_evaluation(:difficulty_with_scope, 3, user2, :s2)).to be true 74 | expect(@phrase.add_evaluation(:difficulty_with_scope, 4, user2, :s3)).to be true 75 | expect(phrase2.add_evaluation(:difficulty_with_scope, 1, user2, :s1)).to be true 76 | expect(phrase2.add_evaluation(:difficulty_with_scope, 2, user2, :s2)).to be true 77 | expect(phrase2.add_evaluation(:difficulty_with_scope, 3, @user, :s2)).to be true 78 | expect(phrase2.add_evaluation(:difficulty_with_scope, 4, @user, :s3)).to be true 79 | expect(Phrase.evaluated_by(:difficulty_with_scope, @user, :s1)).to eq([@phrase]) 80 | expect(Phrase.evaluated_by(:difficulty_with_scope, user2, :s1)).to eq([phrase2]) 81 | expect(Phrase.evaluated_by(:difficulty_with_scope, @user, :s2)).to eq([@phrase, phrase2]) 82 | expect(Phrase.evaluated_by(:difficulty_with_scope, user2, :s2)).to eq([@phrase, phrase2]) 83 | expect(Phrase.evaluated_by(:difficulty_with_scope, @user, :s3)).to eq([phrase2]) 84 | expect(Phrase.evaluated_by(:difficulty_with_scope, user2, :s3)).to eq([@phrase]) 85 | end 86 | end 87 | end 88 | 89 | describe "#evaluation_by" do 90 | it "should return nil if it is not evaluated by a given source" do 91 | expect(@question.evaluation_by(:total_votes, @user)).to be nil 92 | end 93 | 94 | it "should return a value for an evaluation by a given source" do 95 | user2 = User.create!(:name => 'katsuya') 96 | question2 = Question.create!(:text => 'Question 2', :author_id => @user.id) 97 | expect(@question.add_evaluation(:total_votes, 1, @user)).to be true 98 | expect(question2.add_evaluation(:total_votes, 2, user2)).to be true 99 | expect(@question.evaluation_by(:total_votes, @user)).to eq(1) 100 | expect(question2.evaluation_by(:total_votes, user2)).to eq(2) 101 | end 102 | 103 | context "With Scopes" do 104 | it "should return a value for an evaluation by a given source on appropriate scope" do 105 | user2 = User.create!(:name => 'katsuya') 106 | phrase2 = Phrase.create!(:text => "Two") 107 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 108 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 109 | expect(@phrase.add_evaluation(:difficulty_with_scope, 3, user2, :s2)).to be true 110 | expect(@phrase.add_evaluation(:difficulty_with_scope, 4, user2, :s3)).to be true 111 | expect(phrase2.add_evaluation(:difficulty_with_scope, 1, user2, :s1)).to be true 112 | expect(phrase2.add_evaluation(:difficulty_with_scope, 2, user2, :s2)).to be true 113 | expect(phrase2.add_evaluation(:difficulty_with_scope, 3, @user, :s2)).to be true 114 | expect(phrase2.add_evaluation(:difficulty_with_scope, 4, @user, :s3)).to be true 115 | expect(@phrase.evaluation_by(:difficulty_with_scope, @user, :s1)).to eq(1) 116 | expect(@phrase.evaluation_by(:difficulty_with_scope, @user, :s2)).to eq(2) 117 | expect(@phrase.evaluation_by(:difficulty_with_scope, user2, :s2)).to eq(3) 118 | expect(@phrase.evaluation_by(:difficulty_with_scope, user2, :s3)).to eq(4) 119 | expect(phrase2.evaluation_by(:difficulty_with_scope, user2, :s1)).to eq(1) 120 | expect(phrase2.evaluation_by(:difficulty_with_scope, user2, :s2)).to eq(2) 121 | expect(phrase2.evaluation_by(:difficulty_with_scope, @user, :s2)).to eq(3) 122 | expect(phrase2.evaluation_by(:difficulty_with_scope, @user, :s3)).to eq(4) 123 | end 124 | end 125 | end 126 | 127 | describe "#evaluators_for" do 128 | it "should return an empty array if it is not evaluated for a given reputation" do 129 | expect(@question.evaluators_for(:total_votes)).to eq([]) 130 | end 131 | 132 | it "should return an array of sources evaluated the target" do 133 | user2 = User.create!(:name => 'katsuya') 134 | question2 = Question.create!(:text => 'Question 2', :author_id => @user.id) 135 | expect(@question.add_evaluation(:total_votes, 1, @user)).to be true 136 | expect(question2.add_evaluation(:total_votes, 1, @user)).to be true 137 | expect(question2.add_evaluation(:total_votes, 2, user2)).to be true 138 | expect(@question.evaluators_for(:total_votes)).to eq([@user]) 139 | expect(question2.evaluators_for(:total_votes)).to eq([@user, user2]) 140 | end 141 | 142 | context "With Scopes" do 143 | it "should return an array of targets evaluated by a given source on appropriate scope" do 144 | user2 = User.create!(:name => 'katsuya') 145 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 146 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 147 | expect(@phrase.add_evaluation(:difficulty_with_scope, 3, user2, :s2)).to be true 148 | expect(@phrase.add_evaluation(:difficulty_with_scope, 4, user2, :s3)).to be true 149 | expect(@phrase.evaluators_for(:difficulty_with_scope, :s1)).to eq([@user]) 150 | expect(@phrase.evaluators_for(:difficulty_with_scope, :s2)).to eq([@user, user2]) 151 | expect(@phrase.evaluators_for(:difficulty_with_scope, :s3)).to eq([user2]) 152 | end 153 | end 154 | end 155 | 156 | describe "#add_evaluation" do 157 | it "should create evaluation in case of valid input" do 158 | expect(@question.add_evaluation(:total_votes, 1, @user)).to be true 159 | expect(@question.reputation_for(:total_votes)).to eq(1) 160 | end 161 | 162 | it "should raise exception if invalid reputation name is given" do 163 | expect { @question.add_evaluation(:invalid, 1, @user) }.to raise_error(ArgumentError) 164 | end 165 | 166 | it "should raise exception if the same source evaluates for the same target more than once" do 167 | @question.add_evaluation(:total_votes, 1, @user) 168 | expect { @question.add_evaluation(:total_votes, 1, @user) }.to raise_error 169 | end 170 | 171 | it "should not allow the same source to add an evaluation for the same target" do 172 | @question.add_evaluation(:total_votes, 1, @user) 173 | expect { @question.add_evaluation(:total_votes, 1, @user) }.to raise_error 174 | end 175 | 176 | it "should not raise exception if some association has not been initialized along during the propagation of reputation" do 177 | answer = Answer.create! 178 | expect { answer.add_evaluation(:avg_rating, 3, @user) }.not_to raise_error 179 | end 180 | 181 | context "with scopes" do 182 | it "should add evaluation on appropriate scope" do 183 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 184 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 185 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(1) 186 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(2) 187 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 188 | end 189 | 190 | it "should raise exception if invalid scope is given" do 191 | expect { @phrase.add_evaluation(:difficulty_with_scope, 1, :invalid_scope) }.to raise_error(ArgumentError) 192 | end 193 | 194 | it "should raise exception if scope is not given" do 195 | expect { @phrase.add_evaluation(:difficulty_with_scope, 1) }.to raise_error(ArgumentError) 196 | end 197 | end 198 | end 199 | 200 | describe "#add_or_update_evaluation" do 201 | it "should create evaluation if it does not exist" do 202 | expect(@question.add_or_update_evaluation(:total_votes, 1, @user)).to be true 203 | expect(@question.reputation_for(:total_votes)).to eq(1) 204 | end 205 | 206 | it "should update evaluation if it exists already" do 207 | @question.add_evaluation(:total_votes, 1, @user) 208 | expect(@question.add_or_update_evaluation(:total_votes, 2, @user)).to be true 209 | expect(@question.reputation_for(:total_votes)).to eq(2) 210 | end 211 | 212 | context "with scopes" do 213 | it "should add evaluation on appropriate scope if it does not exist" do 214 | expect(@phrase.add_or_update_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 215 | expect(@phrase.add_or_update_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 216 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(1) 217 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(2) 218 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 219 | end 220 | 221 | it "should update evaluation on appropriate scope if it exists already" do 222 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 223 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 224 | expect(@phrase.add_or_update_evaluation(:difficulty_with_scope, 3, @user, :s1)).to be true 225 | expect(@phrase.add_or_update_evaluation(:difficulty_with_scope, 5, @user, :s2)).to be true 226 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(3) 227 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(5) 228 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 229 | end 230 | end 231 | 232 | context "with STI" do 233 | it "should be able to update evaluation by an object of a class with sti" do 234 | @post = Post.create! :name => "Post1" 235 | @designer = Designer.create! :name => "John" 236 | @post.add_or_update_evaluation(:votes, 1, @designer) 237 | @post.add_or_update_evaluation(:votes, -1, @designer) 238 | expect(@post.reputation_for(:votes)).to eq(-1) 239 | end 240 | end 241 | end 242 | 243 | describe "#add_or_delete_evaluation" do 244 | it "should create evaluation if it does not exist" do 245 | expect(@question.add_or_delete_evaluation(:total_votes, 1, @user)).to be true 246 | expect(@question.reputation_for(:total_votes)).to eq(1) 247 | end 248 | 249 | it "should delete evaluation if it exists already" do 250 | @question.add_evaluation(:total_votes, 1, @user) 251 | expect(@question.add_or_delete_evaluation(:total_votes, 2, @user)).to be true 252 | expect(@question.reputation_for(:total_votes)).to eq(0) 253 | end 254 | 255 | context "with scopes" do 256 | it "should add evaluation on appropriate scope if it does not exist" do 257 | expect(@phrase.add_or_delete_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 258 | expect(@phrase.add_or_delete_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 259 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(1) 260 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(2) 261 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 262 | end 263 | 264 | it "should delete evaluation on appropriate scope if it exists already" do 265 | expect(@phrase.add_evaluation(:difficulty_with_scope, 1, @user, :s1)).to be true 266 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 267 | expect(@phrase.add_or_delete_evaluation(:difficulty_with_scope, 3, @user, :s1)).to be true 268 | expect(@phrase.add_or_delete_evaluation(:difficulty_with_scope, 5, @user, :s2)).to be true 269 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 270 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(0) 271 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 272 | end 273 | end 274 | 275 | context "with STI" do 276 | it "should be able to update evaluation by an object of a class with sti" do 277 | @post = Post.create! :name => "Post1" 278 | @designer = Designer.create! :name => "John" 279 | @post.add_or_delete_evaluation(:votes, 1, @designer) 280 | expect(@post.reputation_for(:votes)).to eq(1) 281 | @post.add_or_delete_evaluation(:votes, -1, @designer) 282 | expect(@post.reputation_for(:votes)).to eq(0) 283 | end 284 | end 285 | end 286 | 287 | describe "#update_evaluation" do 288 | before :each do 289 | @question.add_evaluation(:total_votes, 1, @user) 290 | end 291 | 292 | it "should update evaluation in case of valid input" do 293 | expect(@question.update_evaluation(:total_votes, 2, @user)).to be true 294 | expect(@question.reputation_for(:total_votes)).to eq(2) 295 | end 296 | 297 | it "should raise exception if invalid reputation name is given" do 298 | expect { @question.update_evaluation(:invalid, 1, @user) }.to raise_error(ArgumentError) 299 | end 300 | 301 | it "should raise exception if invalid source is given" do 302 | expect { @question.update_evaluation(:total_votes, 1, @answer) }.to raise_error(ArgumentError) 303 | end 304 | 305 | it "should raise exception if evaluation does not exist" do 306 | expect { @answer.update_evaluation(:avg_rating, 1, @user) }.to raise_error 307 | end 308 | 309 | context "With Scopes" do 310 | before :each do 311 | expect(@phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2)).to be true 312 | end 313 | 314 | it "should update evaluation on appropriate scope" do 315 | expect(@phrase.update_evaluation(:difficulty_with_scope, 5, @user, :s2)).to be true 316 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 317 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(5) 318 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 319 | end 320 | 321 | it "should raise exception if invalid scope is given" do 322 | expect { @phrase.update_evaluation(:difficulty_with_scope, 5, @user, :invalid_scope) }.to raise_error(ArgumentError) 323 | end 324 | 325 | it "should raise exception if scope is not given" do 326 | expect { @phrase.update_evaluation(:difficulty_with_scope, 5, @user) }.to raise_error(ArgumentError) 327 | end 328 | end 329 | end 330 | 331 | describe "#delete_evaluation!" do 332 | before :each do 333 | @question.add_evaluation(:total_votes, 1, @user) 334 | end 335 | 336 | it "should delete evaluation in case of valid input" do 337 | @question.delete_evaluation!(:total_votes, @user) 338 | expect(@question.reputation_for(:total_votes)).to eq(0) 339 | end 340 | 341 | it "should raise exception if invalid reputation name is given" do 342 | expect { @question.delete_evaluation!(:invalid, @user) }.to raise_error(ArgumentError) 343 | end 344 | 345 | it "should raise exception if invalid source is given" do 346 | expect { @question.delete_evaluation!(:total_votes, @answer) }.to raise_error(ArgumentError) 347 | end 348 | 349 | it "should raise exception if evaluation does not exist" do 350 | expect { @answer.delete_evaluation!(:avg_rating, @user) }.to raise_error 351 | end 352 | 353 | context "With Scopes" do 354 | before :each do 355 | @phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2) 356 | end 357 | 358 | it "should delete evaluation on appropriate scope" do 359 | @phrase.delete_evaluation!(:difficulty_with_scope, @user, :s2) 360 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 361 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(0) 362 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 363 | end 364 | 365 | it "should raise exception if invalid scope is given" do 366 | expect { @phrase.delete_evaluation!(:difficulty_with_scope, @user, :invalid_scope) }.to raise_error(ArgumentError) 367 | end 368 | 369 | it "should raise exception if scope is not given" do 370 | expect { @phrase.delete_evaluation!(:difficulty_with_scope, @user) }.to raise_error(ArgumentError) 371 | end 372 | end 373 | end 374 | 375 | describe "#delete_evaluation" do 376 | before :each do 377 | @question.add_evaluation(:total_votes, 1, @user) 378 | end 379 | 380 | it "should delete evaluation in case of valid input" do 381 | expect(@question.delete_evaluation(:total_votes, @user)).to be true 382 | expect(@question.reputation_for(:total_votes)).to eq(0) 383 | end 384 | 385 | it "should raise exception if invalid reputation name is given" do 386 | expect { @question.delete_evaluation(:invalid, @user) }.to raise_error(ArgumentError) 387 | end 388 | 389 | it "should return false if evaluation does not exist" do 390 | expect(@answer.delete_evaluation(:avg_rating, @user)).to be false 391 | end 392 | 393 | context "With Scopes" do 394 | before :each do 395 | @phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2) 396 | end 397 | 398 | it "should delete evaluation on appropriate scope" do 399 | expect(@phrase.delete_evaluation(:difficulty_with_scope, @user, :s2)).to be true 400 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 401 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(0) 402 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 403 | end 404 | 405 | it "should raise exception if invalid scope is given" do 406 | expect { @phrase.delete_evaluation(:difficulty_with_scope, @user, :invalid_scope) }.to raise_error(ArgumentError) 407 | end 408 | 409 | it "should raise exception if scope is not given" do 410 | expect { @phrase.delete_evaluation(:difficulty_with_scope, @user) }.to raise_error(ArgumentError) 411 | end 412 | end 413 | end 414 | 415 | describe "#increase_evaluation" do 416 | it "should add evaluation if it does not exist" do 417 | expect(@question.increase_evaluation(:total_votes, 2, @user)).to be true 418 | expect(@question.reputation_for(:total_votes)).to eq(2) 419 | end 420 | 421 | it "should increase evaluation if it exists already" do 422 | @question.add_evaluation(:total_votes, 1, @user) 423 | expect(@question.increase_evaluation(:total_votes, 2, @user)).to be true 424 | expect(@question.reputation_for(:total_votes)).to eq(3) 425 | end 426 | 427 | context "With Scopes" do 428 | before :each do 429 | @phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2) 430 | end 431 | 432 | it "should increase evaluation on appropriate scope" do 433 | expect(@phrase.increase_evaluation(:difficulty_with_scope, 5, @user, :s2)).to be true 434 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 435 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(7) 436 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 437 | end 438 | end 439 | end 440 | 441 | describe "#decrease_evaluation" do 442 | it "should add evaluation if it does not exist" do 443 | expect(@question.decrease_evaluation(:total_votes, 2, @user)).to be true 444 | expect(@question.reputation_for(:total_votes)).to eq(-2) 445 | end 446 | 447 | it "should increase evaluation if it exists already" do 448 | @question.add_evaluation(:total_votes, 1, @user) 449 | expect(@question.decrease_evaluation(:total_votes, 2, @user)).to be true 450 | expect(@question.reputation_for(:total_votes)).to eq(-1) 451 | end 452 | 453 | context "With Scopes" do 454 | before :each do 455 | @phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s2) 456 | end 457 | 458 | it "should decrease evaluation on appropriate scope" do 459 | expect(@phrase.decrease_evaluation(:difficulty_with_scope, 5, @user, :s2)).to be true 460 | expect(@phrase.reputation_for(:difficulty_with_scope, :s1)).to eq(0) 461 | expect(@phrase.reputation_for(:difficulty_with_scope, :s2)).to eq(-3) 462 | expect(@phrase.reputation_for(:difficulty_with_scope, :s3)).to eq(0) 463 | end 464 | end 465 | end 466 | end 467 | 468 | context "Non-Primary Reputation with Gathering Aggregation" do 469 | context "With Scopes" do 470 | before :each do 471 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 472 | @trans_fr = Translation.create!(:text => "Homme", :user => @user, :locale => "fr", :phrase => @phrase) 473 | end 474 | 475 | describe "#add_evaluation" do 476 | it "should affect only reputations with relevant scope" do 477 | @trans_ja.add_evaluation(:votes, 1, @user) 478 | @trans_fr.add_evaluation(:votes, 2, @user) 479 | expect(@phrase.reputation_for(:maturity, :ja)).to eq(1) 480 | expect(@phrase.reputation_for(:maturity, :fr)).to eq(2) 481 | end 482 | end 483 | 484 | describe "#update_evaluation" do 485 | before :each do 486 | @trans_ja.add_evaluation(:votes, 1, @user) 487 | end 488 | 489 | it "should affect only reputations with relevant scope" do 490 | @trans_ja.update_evaluation(:votes, 3, @user) 491 | expect(@phrase.reputation_for(:maturity, :ja)).to eq(3) 492 | expect(@phrase.reputation_for(:maturity, :fr)).to eq(0) 493 | end 494 | end 495 | 496 | describe "#delete_evaluation" do 497 | before :each do 498 | @trans_ja.add_evaluation(:votes, 1, @user) 499 | end 500 | 501 | it "should affect only reputations with relevant scope" do 502 | @trans_ja.delete_evaluation!(:votes, @user) 503 | expect(@phrase.reputation_for(:maturity, :ja)).to eq(0) 504 | expect(@phrase.reputation_for(:maturity, :fr)).to eq(0) 505 | end 506 | end 507 | end 508 | end 509 | 510 | context "Non-Primary Reputation with Mixing Aggregation" do 511 | context "With Scopes" do 512 | before :each do 513 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 514 | @trans_fr = Translation.create!(:text => "Homme", :user => @user, :locale => "fr", :phrase => @phrase) 515 | @trans_de = Translation.create!(:text => "Ein", :user => @user, :locale => "de", :phrase => @phrase) 516 | end 517 | 518 | describe "#add_evaluation" do 519 | it "should affect only reputations with relevant scope" do 520 | @trans_ja.add_evaluation(:votes, 1, @user) 521 | expect(@phrase.reputation_for(:maturity_all)).to eq(1) 522 | @trans_fr.add_evaluation(:votes, 2, @user) 523 | expect(@phrase.reputation_for(:maturity_all)).to eq(3) 524 | @trans_de.add_evaluation(:votes, 3, @user) 525 | expect(@phrase.reputation_for(:maturity_all)).to eq(3) 526 | expect(@phrase.reputation_for(:maturity, :ja)).to eq(1) 527 | expect(@phrase.reputation_for(:maturity, :fr)).to eq(2) 528 | expect(@phrase.reputation_for(:maturity, :de)).to eq(3) 529 | end 530 | end 531 | 532 | describe "#update_evaluation" do 533 | before :each do 534 | @trans_ja.add_evaluation(:votes, 1, @user) 535 | @trans_de.add_evaluation(:votes, 3, @user) 536 | end 537 | 538 | it "should affect only reputations with relevant scope" do 539 | @trans_ja.update_evaluation(:votes, 3, @user) 540 | @trans_de.update_evaluation(:votes, 2, @user) 541 | expect(@phrase.reputation_for(:maturity_all)).to eq(3) 542 | end 543 | end 544 | 545 | describe "#delete_evaluation" do 546 | before :each do 547 | @trans_ja.add_evaluation(:votes, 1, @user) 548 | @trans_de.add_evaluation(:votes, 3, @user) 549 | end 550 | 551 | it "should affect only reputations with relevant scope" do 552 | @trans_de.delete_evaluation!(:votes, @user) 553 | expect(@phrase.reputation_for(:maturity_all)).to eq(1) 554 | @trans_ja.delete_evaluation!(:votes, @user) 555 | expect(@phrase.reputation_for(:maturity_all)).to eq(0) 556 | end 557 | end 558 | end 559 | end 560 | end 561 | -------------------------------------------------------------------------------- /spec/reputation_system/finder_methods_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::FinderMethods do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | describe "#find_with_reputation" do 29 | context "Without Scopes" do 30 | before :each do 31 | @question.add_evaluation(:total_votes, 3, @user) 32 | end 33 | 34 | it "should return result with given reputation" do 35 | res = Question.find_with_reputation(:total_votes, :all, {}) 36 | expect(res).to eq([@question]) 37 | expect(res[0].total_votes).not_to be_nil 38 | end 39 | 40 | it "should retain select option" do 41 | res = Question.find_with_reputation(:total_votes, :all, {:select => "questions.id"}) 42 | expect(res).to eq([@question]) 43 | expect(res[0].id).not_to be_nil 44 | expect {res[0].text}.to raise_error 45 | end 46 | 47 | it "should retain conditions option" do 48 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 49 | @question2.add_evaluation(:total_votes, 5, @user) 50 | res = Question.find_with_reputation(:total_votes, :all, {:conditions => "total_votes > 4"}) 51 | expect(res).to eq([@question2]) 52 | end 53 | 54 | it "should retain joins option" do 55 | res = Question.find_with_reputation(:total_votes, :all, { 56 | :select => "questions.*, users.name AS user_name", 57 | :joins => "JOIN users ON questions.author_id = users.id"}) 58 | expect(res).to eq([@question]) 59 | expect(res[0].user_name).to eq(@user.name) 60 | end 61 | end 62 | 63 | context "With Scopes" do 64 | before :each do 65 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 66 | @trans_ja.add_evaluation(:votes, 3, @user) 67 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 68 | @trans_fr.add_evaluation(:votes, 6, @user) 69 | end 70 | 71 | it "should return result with given reputation" do 72 | res = Phrase.find_with_reputation(:maturity, :ja, :all, {}) 73 | expect(res).to eq([@phrase]) 74 | expect(res[0].maturity).to eq(3) 75 | end 76 | end 77 | end 78 | 79 | describe "#count_with_reputation" do 80 | context "Without Scopes" do 81 | before :each do 82 | @question.add_evaluation(:total_votes, 3, @user) 83 | end 84 | 85 | it "should return result with given reputation" do 86 | expect(Question.count_with_reputation(:total_votes, :all, { 87 | :conditions => "total_votes < 2" 88 | })).to eq(0) 89 | expect(Question.count_with_reputation(:total_votes, :all, { 90 | :conditions => "total_votes > 2" 91 | })).to eq(1) 92 | end 93 | end 94 | 95 | context "With Scopes" do 96 | before :each do 97 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 98 | @trans_ja.add_evaluation(:votes, 3, @user) 99 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 100 | @trans_fr.add_evaluation(:votes, 6, @user) 101 | end 102 | 103 | it "should return result with given reputation" do 104 | expect(Phrase.count_with_reputation(:maturity, :ja, :all, { 105 | :conditions => "maturity < 2" 106 | })).to eq(0) 107 | expect(Phrase.count_with_reputation(:maturity, :ja, :all, { 108 | :conditions => "maturity > 2" 109 | })).to eq(1) 110 | end 111 | end 112 | end 113 | 114 | describe "#find_with_normalized_reputation" do 115 | context "Without Scopes" do 116 | before :each do 117 | @question.add_evaluation(:total_votes, 3, @user) 118 | end 119 | 120 | it "should return result with given normalized reputation" do 121 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 122 | @question2.add_evaluation(:total_votes, 6, @user) 123 | res = Question.find_with_normalized_reputation(:total_votes, :all, {}) 124 | expect(res).to eq([@question, @question2]) 125 | expect(res[0].normalized_total_votes).to be_within(DELTA).of(0) 126 | expect(res[1].normalized_total_votes).to be_within(DELTA).of(1) 127 | end 128 | 129 | it "should retain select option" do 130 | res = Question.find_with_normalized_reputation(:total_votes, :all, {:select => "questions.id"}) 131 | expect(res).to eq([@question]) 132 | expect(res[0].id).not_to be_nil 133 | expect {res[0].text}.to raise_error 134 | end 135 | 136 | it "should retain conditions option" do 137 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 138 | @question2.add_evaluation(:total_votes, 6, @user) 139 | res = Question.find_with_normalized_reputation(:total_votes, :all, {:conditions => "normalized_total_votes > 0.6"}) 140 | expect(res).to eq([@question2]) 141 | end 142 | 143 | it "should retain joins option" do 144 | res = Question.find_with_normalized_reputation(:total_votes, :all, { 145 | :select => "questions.*, users.name AS user_name", 146 | :joins => "JOIN users ON questions.author_id = users.id"}) 147 | expect(res).to eq([@question]) 148 | expect(res[0].user_name).to eq(@user.name) 149 | end 150 | end 151 | 152 | context "With Scopes" do 153 | before :each do 154 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 155 | @trans_ja.add_evaluation(:votes, 3, @user) 156 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 157 | @trans_fr.add_evaluation(:votes, 6, @user) 158 | end 159 | 160 | it "should return result with given reputation" do 161 | res = Phrase.find_with_normalized_reputation(:maturity, :ja, :all, {}) 162 | expect(res).to eq([@phrase]) 163 | expect(res[0].normalized_maturity).to be_within(DELTA).of(0) 164 | end 165 | end 166 | end 167 | 168 | describe "#find_with_reputation_sql" do 169 | it "should return a corresponding sql statement" do 170 | sql = Question.find_with_reputation_sql(:total_votes, :all, { 171 | :select => "questions.*, users.name AS user_name", 172 | :joins => "JOIN users ON questions.author_id = users.id", 173 | :conditions => "COALESCE(rs_reputations.value, 0) > 0.6", 174 | :order => "total_votes"}) 175 | expect(sql).to eq( 176 | "SELECT questions.*, users.name AS user_name, COALESCE(rs_reputations.value, 0) AS total_votes "\ 177 | "FROM \"questions\" JOIN users ON questions.author_id = users.id "\ 178 | "LEFT JOIN rs_reputations ON questions.id = rs_reputations.target_id AND rs_reputations.target_type = 'Question' AND rs_reputations.reputation_name = 'total_votes' AND rs_reputations.active = 't' "\ 179 | "WHERE (COALESCE(rs_reputations.value, 0) > 0.6) "\ 180 | " " 181 | ) if ActiveRecord::VERSION::STRING >= '4' \ 182 | "ORDER BY total_votes" 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /spec/reputation_system/models/evaluation_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::Evaluation do 20 | before(:each) do 21 | @user = User.create!(:name => 'jack') 22 | @question = Question.create!(:text => 'What is Twitter?', :author_id => @user.id) 23 | end 24 | 25 | context "Validation" do 26 | before :each do 27 | @attributes = {:reputation_name => 'total_votes', :source => @user, :target => @question, :value => 1} 28 | end 29 | it "should not be able to create an evaluation from given source if it has already evaluated the same reputation of the target" do 30 | ReputationSystem::Evaluation.create!(@attributes) 31 | expect {ReputationSystem::Evaluation.create!(@attributes)}.to raise_error 32 | end 33 | end 34 | 35 | context "Callback" do 36 | describe "#set_source_type_for_sti" do 37 | it "should assign source class name as source type if not STI" do 38 | question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 39 | question.add_evaluation(:total_votes, 5, @user) 40 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(:total_votes, @user, question) 41 | expect(evaluation.source_type).to eq(@user.class.name) 42 | end 43 | it "should assign source's ancestors class name where reputation is declared if STI" do 44 | designer = Designer.create! :name => 'hiro' 45 | programmer = Programmer.create! :name => 'katsuya' 46 | programmer.add_evaluation(:leadership, 1, designer) 47 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(:leadership, designer, programmer) 48 | expect(evaluation.source_type).to eq(Person.name) 49 | end 50 | end 51 | end 52 | 53 | context "Association" do 54 | it "should delete associated reputation message" do 55 | @question.add_evaluation(:total_votes, 5, @user) 56 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(:total_votes, @user, @question) 57 | expect(ReputationSystem::ReputationMessage.find_by_sender_id_and_sender_type(evaluation.id, evaluation.class.name)).not_to be_nil 58 | @question.delete_evaluation(:total_votes, @user) 59 | expect(ReputationSystem::ReputationMessage.find_by_sender_id_and_sender_type(evaluation.id, evaluation.class.name)).to be_nil 60 | end 61 | end 62 | 63 | context "Additional Data" do 64 | it "should have data as a serialized field" do 65 | @attributes = {:reputation_name => 'total_votes', :source => @user, :target => @question, :value => 1} 66 | e = ReputationSystem::Evaluation.create!(@attributes) 67 | expect(e.data).to be_a(Hash) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/reputation_system/models/reputation_message_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::ReputationMessage do 20 | before(:each) do 21 | @user = User.create!(:name => 'jack') 22 | @rep1 = ReputationSystem::Reputation.create!(:reputation_name => "karma1", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum') 23 | @rep2 = ReputationSystem::Reputation.create!(:reputation_name => "karma2", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum') 24 | end 25 | 26 | context "Validation" do 27 | it "should not be able to create a message from given sender if it has already sent one to the same receiver" do 28 | expect(ReputationSystem::ReputationMessage.create(:sender => @rep1, :receiver => @rep2)).to be_valid 29 | expect(ReputationSystem::ReputationMessage.create(:sender => @rep1, :receiver => @rep2)).not_to be_valid 30 | end 31 | 32 | it "should have raise error if sender is neither ReputationSystem::Evaluation and ReputationSystem::Reputation" do 33 | expect(ReputationSystem::ReputationMessage.create(:sender => @user, :receiver => @rep2).errors[:sender]).not_to be_nil 34 | end 35 | end 36 | 37 | context "Association" do 38 | it "should delete associated sender if it is evaluation" do 39 | question = Question.create!(:text => 'What is Twitter?', :author_id => @user.id) 40 | question.add_evaluation(:total_votes, 5, @user) 41 | evaluation = ReputationSystem::Evaluation.find_by_reputation_name_and_source_and_target(:total_votes, @user, question) 42 | m = ReputationSystem::ReputationMessage.find_by_sender_id_and_sender_type(evaluation.id, evaluation.class.name) 43 | m.destroy 44 | expect { evaluation.reload }.to raise_error ActiveRecord::RecordNotFound 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/reputation_system/models/reputation_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::Reputation do 20 | before(:each) do 21 | @user = User.create!(:name => 'jack') 22 | end 23 | 24 | context "Validation" do 25 | it "should have value 0 by default in case of non product process" do 26 | r = ReputationSystem::Reputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum') 27 | expect(r.value).to eq(0) 28 | end 29 | 30 | it "should be able to change value to 0 if process is not product process" do 31 | r = ReputationSystem::Reputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum', :value => 10) 32 | r.value = 0 33 | r.save! 34 | r.reload 35 | expect(r.value).to eq(0) 36 | end 37 | 38 | it "should have value 1 by default in case of product process" do 39 | r = ReputationSystem::Reputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'product') 40 | expect(r.value).to eq(1) 41 | end 42 | 43 | it "should be able to create reputation with process 'sum', 'average' and 'product'" do 44 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma1", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum')).to be_valid 45 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma2", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'average')).to be_valid 46 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma3", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'product')).to be_valid 47 | end 48 | 49 | it "should be able to create reputation with custom process" do 50 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'custom_process')).to be_valid 51 | end 52 | 53 | it "should be able to create reputation with custom process from source" do 54 | expect(ReputationSystem::Reputation.create(:reputation_name => "custom_rating", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'custom_rating')).to be_valid 55 | end 56 | 57 | it "should not be able to create reputation of the same name for the same target" do 58 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum')).to be_valid 59 | expect(ReputationSystem::Reputation.create(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum')).not_to be_valid 60 | end 61 | end 62 | 63 | context "Callback" do 64 | describe "#set_target_type_for_sti" do 65 | it "should assign target class name as target type if not STI" do 66 | question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 67 | question.add_evaluation(:total_votes, 5, @user) 68 | rep = ReputationSystem::Reputation.find_by_reputation_name_and_target(:total_votes, question) 69 | expect(rep.target_type).to eq(question.class.name) 70 | end 71 | it "should assign target's ancestors class name where reputation is declared if STI" do 72 | designer = Designer.create! :name => 'hiro' 73 | programmer = Programmer.create! :name => 'katsuya' 74 | programmer.add_evaluation(:leadership, 1, designer) 75 | rep = ReputationSystem::Reputation.find_by_reputation_name_and_target(:leadership, programmer) 76 | expect(rep.target_type).to eq(Person.name) 77 | end 78 | end 79 | end 80 | 81 | context "Association" do 82 | before :each do 83 | @question = Question.create!(:text => 'What is Twitter?', :author_id => @user.id) 84 | @question.add_evaluation(:total_votes, 5, @user) 85 | end 86 | 87 | it "should delete associated received messages" do 88 | rep = ReputationSystem::Reputation.find_by_target_id_and_target_type(@question.id, 'Question') 89 | expect(ReputationSystem::ReputationMessage.find_by_receiver_id(rep.id)).not_to be_nil 90 | rep.destroy 91 | expect(ReputationSystem::ReputationMessage.find_by_receiver_id(rep.id)).to be_nil 92 | end 93 | 94 | it "should delete associated sent messages" do 95 | rep = ReputationSystem::Reputation.find_by_target_id_and_target_type(@user.id, 'User') 96 | expect(ReputationSystem::ReputationMessage.find_by_sender_id_and_sender_type(rep.id, rep.class.name)).not_to be_nil 97 | rep.destroy 98 | expect(ReputationSystem::ReputationMessage.find_by_sender_id_and_sender_type(rep.id, rep.class.name)).to be_nil 99 | end 100 | end 101 | 102 | describe "#normalized_value" do 103 | before :each do 104 | @user2 = User.create!(:name => 'dick') 105 | @user3 = User.create!(:name => 'foo') 106 | question = Question.new(:text => "Does this work?", :author_id => @user.id) 107 | @r1 = ReputationSystem::Reputation.create!(:reputation_name => "karma", :value => 2, :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum') 108 | @r2 = ReputationSystem::Reputation.create!(:reputation_name => "karma", :value => 6, :target_id => @user2.id, :target_type => @user2.class.to_s, :aggregated_by => 'sum') 109 | @r3 = ReputationSystem::Reputation.create!(:reputation_name => "karma", :value => 10, :target_id => @user3.id, :target_type => @user3.class.to_s, :aggregated_by => 'sum') 110 | @r4 = ReputationSystem::Reputation.create!(:reputation_name => "karma", :value => 10, :target_id => question.id, :target_type => question.class.to_s, :aggregated_by => 'sum') 111 | end 112 | 113 | it "should return correct normalized value" do 114 | expect(@r1.normalized_value).to be_within(DELTA).of(0) 115 | expect(@r2.normalized_value).to be_within(DELTA).of(0.5) 116 | expect(@r3.normalized_value).to be_within(DELTA).of(1) 117 | end 118 | 119 | it "should return 0 if max and min are the same" do 120 | expect(@r4.normalized_value).to be_within(DELTA).of(0) 121 | end 122 | end 123 | 124 | describe "value propagation with average process" do 125 | it "should calculate average reputation even after evaluation is deleted" do 126 | user1 = User.create! :name => 'dick' 127 | user2 = User.create! :name => 'katsuya' 128 | answer = Answer.create! 129 | answer.add_evaluation(:avg_rating, 3, user1) 130 | answer.add_evaluation(:avg_rating, 2, user2) 131 | expect(answer.reputation_for(:avg_rating)).to be_within(DELTA).of(2.5) 132 | answer.delete_evaluation(:avg_rating, user1) 133 | expect(answer.reputation_for(:avg_rating)).to be_within(DELTA).of(2) 134 | answer.delete_evaluation(:avg_rating, user2) 135 | expect(answer.reputation_for(:avg_rating)).to be_within(DELTA).of(0) 136 | answer.add_evaluation(:avg_rating, 3, user1) 137 | expect(answer.reputation_for(:avg_rating)).to be_within(DELTA).of(3) 138 | end 139 | end 140 | 141 | describe "custom aggregation function" do 142 | it "should calculate based on a custom function for new source" do 143 | user1 = User.create! :name => 'dick' 144 | user2 = User.create! :name => 'katsuya' 145 | answer = Answer.create! 146 | answer.add_or_update_evaluation(:custom_rating, 3, user1) 147 | answer.add_or_update_evaluation(:custom_rating, 2, user2) 148 | expect(answer.reputation_for(:custom_rating)).to be_within(DELTA).of(50) 149 | end 150 | 151 | it "should calculate based on a custom function for updated source" do 152 | user1 = User.create! :name => 'dick' 153 | user2 = User.create! :name => 'katsuya' 154 | answer = Answer.create! 155 | answer.add_or_update_evaluation(:custom_rating, 3, user1) 156 | answer.add_or_update_evaluation(:custom_rating, 2, user1) 157 | expect(answer.reputation_for(:custom_rating)).to be_within(DELTA).of(20) 158 | end 159 | end 160 | 161 | describe "additional data" do 162 | it "should have data as a serialized field" do 163 | r = ReputationSystem::Reputation.create!(:reputation_name => "karma", :target_id => @user.id, :target_type => @user.class.to_s, :aggregated_by => 'sum') 164 | expect(r.data).to be_a(Hash) 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/reputation_system/query_methods_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::QueryMethods do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | describe "#with_reputation" do 29 | context "Without Scopes" do 30 | before :each do 31 | @question.add_evaluation(:total_votes, 3, @user) 32 | end 33 | 34 | it "should return result with given reputation" do 35 | res = Question.with_reputation(:total_votes) 36 | expect(res).to eq([@question]) 37 | expect(res[0].total_votes).not_to be_nil 38 | end 39 | 40 | it "should retain conditions option" do 41 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 42 | @question2.add_evaluation(:total_votes, 5, @user) 43 | res = Question.with_reputation(:total_votes).where("total_votes > 4") 44 | expect(res).to eq([@question2]) 45 | end 46 | 47 | it "should retain joins option" do 48 | res = Question.with_reputation(:total_votes). 49 | select("questions.*, users.name AS user_name"). 50 | joins("JOIN users ON questions.author_id = users.id") 51 | expect(res).to eq([@question]) 52 | expect(res[0].user_name).to eq(@user.name) 53 | end 54 | 55 | it "should not retain select option" do 56 | res = Question.with_reputation(:total_votes).select("questions.id") 57 | expect(res).to eq([@question]) 58 | expect(res[0].id).not_to be_nil 59 | expect {res[0].text}.not_to raise_error 60 | end 61 | end 62 | 63 | context "With Scopes" do 64 | before :each do 65 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 66 | @trans_ja.add_evaluation(:votes, 3, @user) 67 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 68 | @trans_fr.add_evaluation(:votes, 6, @user) 69 | end 70 | 71 | it "should return result with given reputation" do 72 | res = Phrase.with_reputation(:maturity, :ja) 73 | expect(res).to eq([@phrase]) 74 | expect(res[0].maturity).to eq(3) 75 | end 76 | end 77 | end 78 | 79 | describe "#with_reputation_only" do 80 | context "Without Scopes" do 81 | before :each do 82 | @question.add_evaluation(:total_votes, 3, @user) 83 | end 84 | 85 | it "should return result with given reputation" do 86 | res = Question.with_reputation_only(:total_votes) 87 | expect(res.length).to eq(1) 88 | expect(res[0].total_votes).not_to be_nil 89 | end 90 | 91 | it "should retain conditions option" do 92 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 93 | @question2.add_evaluation(:total_votes, 5, @user) 94 | res = Question.with_reputation_only(:total_votes).where("total_votes > 4") 95 | expect(res.length).to eq(1) 96 | expect(res[0].total_votes).to be > 4 97 | end 98 | 99 | it "should retain joins option" do 100 | res = Question.with_reputation_only(:total_votes). 101 | select("questions.*, users.name AS user_name"). 102 | joins("JOIN users ON questions.author_id = users.id") 103 | expect(res[0].user_name).to eq(@user.name) 104 | end 105 | 106 | it "should retain select option" do 107 | res = Question.with_reputation_only(:total_votes).select("questions.id") 108 | expect(res).to eq([@question]) 109 | expect(res[0].id).not_to be_nil 110 | expect {res[0].text}.to raise_error 111 | end 112 | end 113 | 114 | context "With Scopes" do 115 | before :each do 116 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 117 | @trans_ja.add_evaluation(:votes, 3, @user) 118 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 119 | @trans_fr.add_evaluation(:votes, 6, @user) 120 | end 121 | 122 | it "should return result with given reputation" do 123 | res = Phrase.with_reputation_only(:maturity, :ja) 124 | expect(res.length).to eq(1) 125 | expect(res[0].maturity).to eq(3) 126 | end 127 | end 128 | end 129 | 130 | describe "#with_normalized_reputation" do 131 | context "Without Scopes" do 132 | before :each do 133 | @question.add_evaluation(:total_votes, 3, @user) 134 | end 135 | 136 | it "should return result with given normalized reputation" do 137 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 138 | @question2.add_evaluation(:total_votes, 6, @user) 139 | res = Question.with_normalized_reputation(:total_votes) 140 | expect(res).to eq([@question, @question2]) 141 | expect(res[0].normalized_total_votes).to be_within(DELTA).of(0) 142 | expect(res[1].normalized_total_votes).to be_within(DELTA).of(1) 143 | end 144 | 145 | it "should not retain select option" do 146 | res = Question.with_normalized_reputation(:total_votes).select("questions.id") 147 | expect(res).to eq([@question]) 148 | expect(res[0].id).not_to be_nil 149 | expect {res[0].text}.not_to raise_error 150 | end 151 | 152 | it "should retain conditions option" do 153 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 154 | @question2.add_evaluation(:total_votes, 6, @user) 155 | res = Question.with_normalized_reputation(:total_votes).where("normalized_total_votes > 0.6") 156 | expect(res).to eq([@question2]) 157 | end 158 | 159 | it "should retain joins option" do 160 | res = Question.with_normalized_reputation(:total_votes). 161 | select("questions.*, users.name AS user_name"). 162 | joins("JOIN users ON questions.author_id = users.id") 163 | expect(res).to eq([@question]) 164 | expect(res[0].user_name).to eq(@user.name) 165 | end 166 | end 167 | 168 | context "With Scopes" do 169 | before :each do 170 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 171 | @trans_ja.add_evaluation(:votes, 3, @user) 172 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 173 | @trans_fr.add_evaluation(:votes, 6, @user) 174 | end 175 | 176 | it "should return result with given reputation" do 177 | res = Phrase.with_normalized_reputation(:maturity, :ja) 178 | expect(res).to eq([@phrase]) 179 | expect(res[0].normalized_maturity).to be_within(DELTA).of(0) 180 | end 181 | end 182 | end 183 | 184 | describe "#with_normalized_reputation_only" do 185 | context "Without Scopes" do 186 | before :each do 187 | @question.add_evaluation(:total_votes, 3, @user) 188 | end 189 | 190 | it "should return result with given normalized reputation" do 191 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 192 | @question2.add_evaluation(:total_votes, 6, @user) 193 | res = Question.with_normalized_reputation_only(:total_votes) 194 | expect(res.length).to eq(2) 195 | expect(res[0].normalized_total_votes).to be_within(DELTA).of(0) 196 | expect(res[1].normalized_total_votes).to be_within(DELTA).of(1) 197 | end 198 | 199 | it "should not retain select option" do 200 | res = Question.with_normalized_reputation_only(:total_votes).select("questions.id") 201 | expect(res.length).to eq(1) 202 | expect(res[0].id).not_to be_nil 203 | expect {res[0].text}.to raise_error 204 | end 205 | 206 | it "should retain conditions option" do 207 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 208 | @question2.add_evaluation(:total_votes, 6, @user) 209 | res = Question.with_normalized_reputation_only(:total_votes).where("normalized_total_votes > 0.6") 210 | expect(res.length).to eq(1) 211 | expect(res[0].normalized_total_votes).to be > 0.6 212 | end 213 | 214 | it "should retain joins option" do 215 | res = Question.with_normalized_reputation_only(:total_votes). 216 | select("questions.*, users.name AS user_name"). 217 | joins("JOIN users ON questions.author_id = users.id") 218 | expect(res.length).to eq(1) 219 | expect(res[0].user_name).to eq(@user.name) 220 | end 221 | end 222 | 223 | context "With Scopes" do 224 | before :each do 225 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 226 | @trans_ja.add_evaluation(:votes, 3, @user) 227 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "fr", :phrase => @phrase) 228 | @trans_fr.add_evaluation(:votes, 6, @user) 229 | end 230 | 231 | it "should return result with given reputation" do 232 | res = Phrase.with_normalized_reputation_only(:maturity, :ja) 233 | expect(res.length).to eq(1) 234 | expect(res[0].normalized_maturity).to be_within(DELTA).of(0) 235 | end 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/reputation_system/reputation_methods_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::ReputationMethods do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | context "Primary Reputation" do 29 | describe "#reputation_for" do 30 | it "should return 0 as a default" do 31 | expect(@question.reputation_for(:total_votes)).to eq(0) 32 | end 33 | 34 | it "should return appropriate value in case of valid input" do 35 | user2 = User.new(:name => 'dick') 36 | @question.add_evaluation(:total_votes, 1, @user) 37 | @question.add_evaluation(:total_votes, 1, user2) 38 | expect(@question.reputation_for(:total_votes)).to eq(2) 39 | end 40 | 41 | it "should raise exception if invalid reputation name is given" do 42 | expect {@question.reputation_for(:invalid)}.to raise_error(ArgumentError) 43 | end 44 | 45 | it "should raise exception if scope is given for reputation with no scopes" do 46 | expect {@question.reputation_for(:difficulty, :s1)}.to raise_error(ArgumentError) 47 | end 48 | 49 | it "should raise exception if scope is not given for reputation with scopes" do 50 | expect {@phrase.reputation_for(:difficulty_with_scope)}.to raise_error(ArgumentError) 51 | end 52 | end 53 | 54 | describe "#rank_for" do 55 | context "without scope" do 56 | before :each do 57 | @question.add_evaluation(:total_votes, 3, @user) 58 | @question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 59 | @question2.add_evaluation(:total_votes, 5, @user) 60 | end 61 | 62 | it "should return rank properly" do 63 | expect(@question.rank_for(:total_votes)).to eq(2) 64 | expect(@question2.rank_for(:total_votes)).to eq(1) 65 | end 66 | end 67 | 68 | context "with scope" do 69 | before :each do 70 | @trans_ja = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase) 71 | @trans_ja.add_evaluation(:votes, 3, @user) 72 | @phrase2 = Phrase.create!(:text => "One") 73 | @trans_fr = Translation.create!(:text => "Ichi", :user => @user, :locale => "ja", :phrase => @phrase2) 74 | @trans_fr.add_evaluation(:votes, 6, @user) 75 | end 76 | 77 | it "should return rank properly" do 78 | expect(@phrase.rank_for(:maturity, :ja)).to eq(2) 79 | expect(@phrase2.rank_for(:maturity, :ja)).to eq(1) 80 | end 81 | end 82 | end 83 | end 84 | 85 | context "Non-Primary Reputation with Gathering Aggregation" do 86 | describe "#reputation_for" do 87 | it "should always have correct updated value" do 88 | question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 89 | expect(@user.reputation_for(:question_karma)).to eq(0) 90 | @question.add_evaluation(:total_votes, 1, @user) 91 | expect(@user.reputation_for(:question_karma)).to eq(1) 92 | question2.add_evaluation(:total_votes, 1, @user) 93 | expect(@user.reputation_for(:question_karma)).to eq(2) 94 | end 95 | end 96 | end 97 | 98 | context "Non-Primary Reputation with Mixing Aggregation" do 99 | describe "#reputation_for" do 100 | it "should always have correct updated value" do 101 | question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 102 | question2 = Question.create!(:text => 'Does this work?', :author_id => @user.id) 103 | question.add_evaluation(:difficulty, 1, @user) 104 | question2.add_evaluation(:difficulty, 2, @user) 105 | question.add_evaluation(:total_votes, 1, @user) 106 | question2.add_evaluation(:total_votes, 1, @user) 107 | answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => question.id) 108 | answer2 = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => question2.id) 109 | answer.add_evaluation(:avg_rating, 3, @user) 110 | answer2.add_evaluation(:avg_rating, 2, @user) 111 | expect(answer.reputation_for(:weighted_avg_rating)).to eq(3) 112 | expect(answer2.reputation_for(:weighted_avg_rating)).to eq(4) 113 | expect(@user.reputation_for(:answer_karma)).to be_within(DELTA).of(3.5) 114 | expect(@user.reputation_for(:question_karma)).to be_within(DELTA).of(2) 115 | expect(@user.reputation_for(:karma)).to be_within(DELTA).of(1.4) 116 | end 117 | end 118 | end 119 | 120 | context "Normalization" do 121 | describe "#normalized_reputation_for" do 122 | it "should return 0 as if there is no data" do 123 | expect(@question.normalized_reputation_for(:total_votes)).to eq(0) 124 | end 125 | 126 | it "should return appropriate value in case of valid input" do 127 | question2 = Question.create!(:text => 'Does this work too?', :author_id => @user.id) 128 | question3 = Question.create!(:text => 'Does this work too?', :author_id => @user.id) 129 | @question.add_evaluation(:total_votes, 1, @user) 130 | question2.add_evaluation(:total_votes, 2, @user) 131 | question3.add_evaluation(:total_votes, 3, @user) 132 | expect(@question.normalized_reputation_for(:total_votes)).to eq(0) 133 | expect(question2.normalized_reputation_for(:total_votes)).to eq(0.5) 134 | expect(question3.normalized_reputation_for(:total_votes)).to eq(1) 135 | end 136 | 137 | it "should raise exception if invalid reputation name is given" do 138 | expect {@question.normalized_reputation_for(:invalid)}.to raise_error(ArgumentError) 139 | end 140 | 141 | it "should raise exception if scope is given for reputation with no scopes" do 142 | expect {@question.normalized_reputation_for(:difficulty, :s1)}.to raise_error(ArgumentError) 143 | end 144 | 145 | it "should raise exception if scope is not given for reputation with scopes" do 146 | expect {@phrase.normalized_reputation_for(:difficulty_with_scope)}.to raise_error(ArgumentError) 147 | end 148 | end 149 | 150 | describe "#exclude_all_reputations_for_normalization" do 151 | it "should activate all reputation" do 152 | @question2 = Question.create!(:text => 'Does this work??', :author_id => @user.id) 153 | @question2.add_evaluation(:total_votes, 70, @user) 154 | @question.add_evaluation(:total_votes, 100, @user) 155 | @question.deactivate_all_reputations 156 | expect(ReputationSystem::Reputation.where(:reputation_name => 'total_votes', :active => true).maximum(:value)).to eq(70) 157 | @question.activate_all_reputations 158 | expect(ReputationSystem::Reputation.where(:reputation_name => 'total_votes', :active => true).maximum(:value)).to eq(100) 159 | end 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /spec/reputation_system/scope_methods_spec.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'spec_helper' 18 | 19 | describe ReputationSystem::ScopeMethods do 20 | 21 | before(:each) do 22 | @user = User.create!(:name => 'jack') 23 | @question = Question.create!(:text => 'Does this work?', :author_id => @user.id) 24 | @answer = Answer.create!(:text => 'Yes!', :author_id => @user.id, :question_id => @question.id) 25 | @phrase = Phrase.create!(:text => "One") 26 | end 27 | 28 | describe "#add_scope_for" do 29 | it "should add scope if the reputation has scopes defined" do 30 | Phrase.add_scope_for(:difficulty_with_scope, :s4) 31 | @phrase.add_evaluation(:difficulty_with_scope, 2, @user, :s4) 32 | expect(@phrase.reputation_for(:difficulty_with_scope, :s4)).to eq(2) 33 | end 34 | 35 | it "should raise exception if the scope already exist" do 36 | expect{Phrase.add_scope_for(:difficulty_with_scope, :s1)}.to raise_error(ArgumentError) 37 | end 38 | 39 | it "should raise exception if the reputation does not have scopes defined" do 40 | expect{Question.add_scope_for(:difficulty, :s1)}.to raise_error(ArgumentError) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Copyright 2012 Twitter, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ## 16 | 17 | require 'active_record' 18 | require 'database_cleaner' 19 | require 'sqlite3' 20 | require 'reputation_system' 21 | 22 | DELTA = 0.000001 23 | 24 | ActiveRecord::Base.establish_connection( 25 | :adapter => "sqlite3", 26 | :database => ":memory:" 27 | ) 28 | 29 | RSpec.configure do |config| 30 | config.before(:each) do 31 | DatabaseCleaner.start 32 | end 33 | config.after(:each) do 34 | DatabaseCleaner.clean 35 | end 36 | end 37 | 38 | ActiveRecord::Migration.verbose = false 39 | 40 | ActiveRecord::Schema.define do 41 | create_table :rs_evaluations do |t| 42 | t.string :reputation_name 43 | t.references :source, :polymorphic => true 44 | t.references :target, :polymorphic => true 45 | t.float :value, :default => 0 46 | t.text :data 47 | t.timestamps 48 | end 49 | 50 | add_index :rs_evaluations, :reputation_name 51 | add_index :rs_evaluations, [:target_id, :target_type] 52 | add_index :rs_evaluations, [:source_id, :source_type] 53 | 54 | create_table :rs_reputations do |t| 55 | t.string :reputation_name 56 | t.float :value, :default => 0 57 | t.string :aggregated_by 58 | t.references :target, :polymorphic => true 59 | t.boolean :active, :default => true 60 | t.text :data 61 | t.timestamps 62 | end 63 | 64 | add_index :rs_reputations, :reputation_name 65 | add_index :rs_reputations, [:target_id, :target_type] 66 | 67 | create_table :rs_reputation_messages do |t| 68 | t.references :sender, :polymorphic => true 69 | t.integer :receiver_id 70 | t.float :weight, :default => 1 71 | t.timestamps 72 | end 73 | 74 | add_index :rs_reputation_messages, [:sender_id, :sender_type] 75 | add_index :rs_reputation_messages, :receiver_id 76 | 77 | create_table :users do |t| 78 | t.string :name 79 | t.timestamps 80 | end 81 | 82 | create_table :answers do |t| 83 | t.integer :author_id 84 | t.integer :question_id 85 | t.string :text 86 | t.timestamps 87 | end 88 | 89 | create_table :questions do |t| 90 | t.integer :author_id 91 | t.string :text 92 | t.timestamps 93 | end 94 | 95 | create_table :phrases do |t| 96 | t.string :text 97 | t.timestamps 98 | end 99 | 100 | create_table :translations do |t| 101 | t.integer :user_id 102 | t.integer :phrase_id 103 | t.string :text 104 | t.string :locale 105 | t.timestamps 106 | end 107 | 108 | create_table :people do |t| 109 | t.string :name 110 | t.string :type 111 | t.timestamps 112 | end 113 | 114 | create_table :posts do |t| 115 | t.string :name 116 | t.timestamps 117 | end 118 | end 119 | 120 | class User < ActiveRecord::Base 121 | has_many :answers, :foreign_key => 'author_id', :class_name => 'Answer' 122 | has_many :questions, :foreign_key => 'author_id', :class_name => 'Question' 123 | 124 | has_reputation :karma, 125 | :source => [ 126 | { :reputation => :question_karma }, 127 | { :reputation => :answer_karma, :weight => 0.2 }], 128 | :aggregated_by => :product 129 | 130 | has_reputation :question_karma, 131 | :source => { :reputation => :total_votes, :of => :questions }, 132 | :aggregated_by => :sum 133 | 134 | has_reputation :answer_karma, 135 | :source => { :reputation => :weighted_avg_rating, :of => :answers }, 136 | :aggregated_by => :average 137 | 138 | has_reputation :custom_rating, 139 | :source => { :reputation => :custom_rating, :of => :answers }, 140 | :aggregated_by => :custom_rating 141 | 142 | def custom_process 143 | 123 144 | end 145 | end 146 | 147 | class Question < ActiveRecord::Base 148 | belongs_to :author, :class_name => 'User' 149 | has_many :answers 150 | 151 | has_reputation :total_votes, 152 | :source => :user, 153 | :source_of => { :reputation => :question_karma, :of => :author } 154 | 155 | has_reputation :difficulty, 156 | :source => :user, 157 | :aggregated_by => :average 158 | end 159 | 160 | class Answer < ActiveRecord::Base 161 | belongs_to :author, :class_name => 'User' 162 | belongs_to :question 163 | 164 | has_reputation :weighted_avg_rating, 165 | :source => [ 166 | { :reputation => :avg_rating }, 167 | { :reputation => :difficulty, :of => :question }], 168 | :aggregated_by => :product, 169 | :source_of => { :reputation => :answer_karma, :of => :author } 170 | 171 | has_reputation :avg_rating, 172 | :source => :user, 173 | :aggregated_by => :average 174 | 175 | has_reputation :custom_rating, 176 | :source => :user, 177 | :aggregated_by => :custom_aggregation, 178 | :source_of => { :reputation => :custom_rating, :of => :author } 179 | 180 | def custom_aggregation(*args) 181 | rep, source, weight = args[0..2] 182 | # rep, source, weight 183 | if args.length === 3 184 | rep.value + weight * source.value * 10 185 | # rep, source, weight, oldValue, newSize 186 | elsif args.length === 5 187 | oldValue, newSize = args[3..4] 188 | rep.value + (source.value - oldValue) * 10 189 | end 190 | end 191 | end 192 | 193 | class Phrase < ActiveRecord::Base 194 | has_many :translations do 195 | def for(locale) 196 | self.where(:locale => locale.to_s).to_a 197 | end 198 | end 199 | 200 | has_reputation :maturity_all, 201 | :source => [ 202 | { :reputation => :maturity, :of => :self, :scope => :ja }, 203 | { :reputation => :maturity, :of => :self, :scope => :fr }], 204 | :aggregated_by => :sum 205 | 206 | has_reputation :maturity, 207 | :source => { :reputation => :votes, :of => lambda {|this, s| this.translations.for(s)} }, 208 | :aggregated_by => :sum, 209 | :scopes => [:ja, :fr, :de], 210 | :source_of => { :reputation => :maturity_all, :of => :self, :defined_for_scope => [:ja, :fr] } 211 | 212 | has_reputation :maturity_of_all_translations, 213 | :source => { :reputation => :votes, :of => :translations }, 214 | :aggregated_by => :sum, 215 | :scopes => [:ja, :fr, :de] 216 | 217 | has_reputation :difficulty_with_scope, 218 | :source => :user, 219 | :aggregated_by => :average, 220 | :scopes => [:s1, :s2, :s3] 221 | end 222 | 223 | class Translation < ActiveRecord::Base 224 | belongs_to :user 225 | belongs_to :phrase 226 | 227 | has_reputation :votes, 228 | :source => :user, 229 | :aggregated_by => :sum, 230 | :source_of => [ 231 | { :reputation => :maturity, :of => :phrase, :scope => :locale}, 232 | { :reputation => :maturity_of_all_translations, :of => :phrase, :scope => :locale} 233 | ] 234 | end 235 | 236 | # For STI Specs 237 | 238 | class Person < ActiveRecord::Base 239 | has_reputation :leadership, 240 | :source => :person, 241 | :aggregated_by => :sum 242 | end 243 | 244 | class Programmer < Person 245 | end 246 | 247 | class Designer < Person 248 | end 249 | 250 | class Post < ActiveRecord::Base 251 | belongs_to :person 252 | 253 | has_reputation :votes, 254 | :source => :person 255 | end 256 | --------------------------------------------------------------------------------