merit 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c0822239c7421032c0da3487684baadd738b4805
4
- data.tar.gz: 44eb5162425fc37edb09615e7f95f112d6b8cf04
3
+ metadata.gz: 62f239d505d3f998184052a8cc62602609fcc06d
4
+ data.tar.gz: c1d5bdba54829b26022f9b68322025574e0078df
5
5
  SHA512:
6
- metadata.gz: 5379050b40dd4b93083443c349cf0726ee7fa436708cb2d8a4456f86b1a2b6de5c0b2dae51205613bca4b39fb5ea6bd9afded0d45d07fbfa962654284b31c08c
7
- data.tar.gz: 346cf4cd10a20c4bfb07b1579cf16291ec77b6906b1f22999444bc7950b797d3fc372dfc4a63e8e393c56117b211e33ca571d8bacd1a8c7c3de5209c4130f8d9
6
+ metadata.gz: e58ae9a432d4d9a74cf5fed778acee101d39d17cef09a3d8e8fcdd994589d5ce1d091fda10f103d45e3e2a0bfe22b83ae42e536040717badf6586baea0c2cefe
7
+ data.tar.gz: b1d067b943eb541adf433b6e775d8bd9ba01d606897ccec084f57aff26c055ffa9c248f3c444356fc1f3b576fd9e17e0e9372b1d046a16f74e794a3eeb77270a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ - [#148] Mongoid support
6
+ - Docs, tests, internals polishing.
7
+
3
8
  ## 2.0.0
4
9
 
5
10
  - [#146] Fixes loading paths
data/Gemfile CHANGED
@@ -22,12 +22,12 @@ case ENV['ORM']
22
22
  when 'active_record'
23
23
  gem 'activerecord'
24
24
  when 'mongoid'
25
- gem 'mongoid', '3.0.10'
25
+ gem 'mongoid', '3.1.0'
26
26
  end
27
27
 
28
28
  group :development, :test do
29
29
  gem 'activerecord-jdbcsqlite3-adapter', :platforms => [:jruby]
30
- gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw]
30
+ gem 'sqlite3', '1.3.8', :platforms => [:ruby, :mswin, :mingw]
31
31
  end
32
32
 
33
33
  platforms :rbx do
@@ -36,3 +36,5 @@ platforms :rbx do
36
36
  gem 'rubysl-test-unit'
37
37
  gem 'rubinius-developer_tools'
38
38
  end
39
+
40
+ gem 'coveralls', require: false
data/README.md CHANGED
@@ -4,6 +4,8 @@ Merit adds reputation behavior to Rails apps in the form of Badges, Points,
4
4
  and Rankings.
5
5
 
6
6
  [![Build Status](https://travis-ci.org/tute/merit.png?branch=master)](http://travis-ci.org/tute/merit)
7
+ [![Coverage
8
+ Status](https://coveralls.io/repos/tute/merit/badge.png?branch=master)](https://coveralls.io/r/tute/merit?branch=master)
7
9
  [![Code Climate](https://codeclimate.com/github/tute/merit.png)](https://codeclimate.com/github/tute/merit)
8
10
 
9
11
 
@@ -36,7 +38,8 @@ and Rankings.
36
38
  2. Run `rails g merit:install`
37
39
  3. Run `rails g merit MODEL_NAME` (e.g. `user`)
38
40
  4. Run `rake db:migrate`
39
- 5. Define badges in `config/initializers/merit.rb`
41
+ 5. Define badges in `config/initializers/merit.rb`. You can also define ORM:
42
+ `:active_record` (default) or `:mongoid`.
40
43
  6. Configure reputation rules for your application in `app/models/merit/*`
41
44
 
42
45
 
@@ -77,13 +80,15 @@ Badge rules / conditions are defined in `app/models/merit/badge_rules.rb`
77
80
  * `'controller#action'` a string similar to Rails routes
78
81
  * `:badge` corresponds to the `:name` of the badge
79
82
  * `:level` corresponds to the `:level` of the badge
80
- * `:to` the object's field to give the badge to
81
- * If you are putting badges on the related user then this field is probably
82
- `:user`.
83
- * Needs a variable named `@model` in the associated controller action, like
84
- `@post` for `posts_controller.rb` or `@comment` for `comments_controller.rb`.
85
- Implementation note: Merit finds the object with following snippet:
86
- `instance_variable_get(:"@#{controller_name.singularize}")`.
83
+ * `:to` the object's field to give the badge to. It needs a variable named
84
+ `@model` in the associated controller action, like `@post` for
85
+ `posts_controller.rb` or `@comment` for `comments_controller.rb`.
86
+ * Can be a method name, which called over the target object should retrieve
87
+ the object to badge. If it's `:user` for example, merit will internally
88
+ call `@model.user` to find who to badge.
89
+ * Can be `:itself`, in which case it badges the target object itself
90
+ (`@model`).
91
+ * Is `:action_user` by default, which means `current_user`.
87
92
  * `:model_name` define the controller's name if it's different from
88
93
  the model's (e.g. `RegistrationsController` for the `User` model).
89
94
  * `:multiple` whether or not the badge may be granted multiple times. `false` by default.
@@ -266,17 +271,31 @@ To do so, add your observer (to `app/models` or `app/observers`, for example):
266
271
  # reputation_change_observer.rb
267
272
  class ReputationChangeObserver
268
273
  def update(changed_data)
269
- # `changed_data[:description]` holds information on what changed
270
- # badges granted or removed, points changed.
271
-
272
- # `changed_data[:merit_object]` reputation related object created by merit.
273
- # It responds to `sash_id` and `sash`. From there you can get to your
274
- # application object that had it's reputation changed, for example:
275
- # sash_id = changed_data[:merit_object].sash_id
276
- # User.where(sash_id: sash_id).first
277
-
278
- # You may use this to fill a timeline with notifications for users, send
279
- # emails, etc.
274
+ # description will be something like:
275
+ # granted 5 points
276
+ # granted just-registered badge
277
+ # removed autobiographer badge
278
+ description = changed_data[:description]
279
+
280
+ # If user is your meritable model, you can grab it like:
281
+ if changed_data[:merit_object]
282
+ sash_id = changed_data[:merit_object].sash_id
283
+ user = User.where(sash_id: sash_id).first
284
+ end
285
+
286
+ # To know where and when it happened:
287
+ merit_action = Merit::Action.find changed_data[:merit_action_id]
288
+ controller = merit_action.target_model
289
+ action = merit_action.action_method
290
+ when = merit_action.created_at
291
+
292
+ # From here on, you can create a new Notification assuming that's an
293
+ # ActiveRecord Model in your app, send an email, etc. For example:
294
+ Notification.create(
295
+ user: user,
296
+ what: description,
297
+ where: "#{controller}##{action}",
298
+ when: when)
280
299
  end
281
300
  end
282
301
  ```
@@ -285,6 +304,10 @@ end
285
304
  config.add_observer 'ReputationChangeObserver'
286
305
  ```
287
306
 
307
+ TODO: Improve API sending in `changed_data` concrete data instead of merit
308
+ objects.
309
+
310
+
288
311
  # Uninstalling Merit
289
312
 
290
313
  1. Run `rails d merit:install`
@@ -6,7 +6,7 @@ module Merit
6
6
  extend Ambry::Model
7
7
  extend Ambry::ActiveModel
8
8
 
9
- field :id, :name, :level, :image, :description, :custom_fields
9
+ field :id, :name, :level, :description, :custom_fields
10
10
 
11
11
  validates_presence_of :id, :name
12
12
  validates_uniqueness_of :id
@@ -26,6 +26,14 @@ module Merit
26
26
  end
27
27
  end
28
28
 
29
+ def _mongoid_sash_in(sashes)
30
+ {:sash_id.in => sashes}
31
+ end
32
+
33
+ def _active_record_sash_in(sashes)
34
+ {sash_id: sashes}
35
+ end
36
+
29
37
  class << self
30
38
  def find_by_name_and_level(name, level)
31
39
  badges = Badge.by_name(name)
@@ -48,10 +56,11 @@ module Merit
48
56
 
49
57
  # Defines Badge#meritable_models method, to get related
50
58
  # entries with certain badge. For instance, Badge.find(3).users
59
+ # orm-specified
51
60
  def _define_related_entries_method(meritable_class_name)
52
61
  define_method(:"#{meritable_class_name.underscore.pluralize}") do
53
62
  sashes = BadgesSash.where(badge_id: id).pluck(:sash_id)
54
- meritable_class_name.constantize.where(sash_id: sashes)
63
+ meritable_class_name.constantize.where(send "_#{Merit.orm}_sash_in", sashes)
55
64
  end
56
65
  end
57
66
  end
@@ -48,10 +48,19 @@ module Merit
48
48
  target_id = target_object.try(:id)
49
49
  # If target_id is nil
50
50
  # then use params[:id].
51
- if target_id.nil? && params[:id].to_s =~ /^[0-9]+$/
51
+ if target_id.nil? && send("check_#{Merit.orm}_id", params[:id])
52
52
  target_id = params[:id]
53
53
  end
54
54
  target_id
55
55
  end
56
+
57
+ # This check avoids trying to set a slug as integer FK
58
+ def check_active_record_id(id)
59
+ id.to_s =~ /^[0-9]+$/
60
+ end
61
+
62
+ def check_mongoid_id(id)
63
+ id.to_s =~ /^[0-9a-fA-F]{24}$/
64
+ end
56
65
  end
57
66
  end
@@ -7,7 +7,8 @@ module Merit
7
7
  # That's why MeritableModel belongs_to Sash. Can't use
8
8
  # dependent: destroy as it may raise FK constraint exceptions. See:
9
9
  # https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/1079-belongs_to-dependent-destroy-should-destroy-self-before-assocation
10
- belongs_to :sash, class_name: 'Merit::Sash'
10
+ belongs_to :sash, class_name: 'Merit::Sash', inverse_of: nil
11
+ attr_accessible :sash if show_attr_accessible?
11
12
 
12
13
  send :"_merit_#{Merit.orm}_specific_config"
13
14
  _merit_delegate_methods_to_sash
@@ -48,7 +49,7 @@ module Merit
48
49
  # http://blog.hasmanythrough.com/2012/1/20/modularized-association-methods-in-rails-3-2
49
50
  def _merit_sash_initializer
50
51
  define_method(:_sash) do
51
- sash || update_attribute(:sash_id, Sash.create.id)
52
+ sash || update_attributes(sash: Sash.create)
52
53
  sash
53
54
  end
54
55
  end
@@ -12,14 +12,18 @@ module Merit
12
12
 
13
13
  after_create :create_scores
14
14
 
15
- def add_badge(badge_id)
16
- bs = BadgesSash.new(badge_id: badge_id)
17
- badges_sashes << bs
18
- bs
19
- end
20
-
21
- def rm_badge(badge_id)
22
- badges_sashes.find_by_badge_id(badge_id).try(:destroy)
15
+ # Retrieve all points from a category or none if category doesn't exist
16
+ # By default retrieve all Points
17
+ # @param category [String] The category
18
+ # @return [ActiveRecord::Relation] containing the points
19
+ def score_points(options = {})
20
+ scope = Merit::Score::Point
21
+ .includes(:score)
22
+ .where('merit_scores.sash_id = ?', id)
23
+ if (category = options[:category])
24
+ scope = scope.where('merit_scores.category = ?', category)
25
+ end
26
+ scope
23
27
  end
24
28
  end
25
29
  end
@@ -16,6 +16,7 @@ module Merit
16
16
  has_many :activity_logs,
17
17
  class_name: 'Merit::ActivityLog',
18
18
  as: :related_change
19
+ delegate :sash_id, to: :score
19
20
  end
20
21
  end
21
22
  end
@@ -9,6 +9,16 @@ module Merit
9
9
  badges_sashes.map(&:badge_id)
10
10
  end
11
11
 
12
+ def add_badge(badge_id)
13
+ bs = Merit::BadgesSash.new(badge_id: badge_id)
14
+ badges_sashes << bs
15
+ bs
16
+ end
17
+
18
+ def rm_badge(badge_id)
19
+ badges_sashes.where(badge_id: badge_id).first.try(:destroy)
20
+ end
21
+
12
22
  # Retrieve the number of points from a category
13
23
  # By default all points are summed up
14
24
  # @param category [String] The category
@@ -21,20 +31,6 @@ module Merit
21
31
  end
22
32
  end
23
33
 
24
- # Retrieve all points from a category or none if category doesn't exist
25
- # By default retrieve all Points
26
- # @param category [String] The category
27
- # @return [ActiveRecord::Relation] containing the points
28
- def score_points(options = {})
29
- scope = Merit::Score::Point
30
- .includes(:score)
31
- .where('merit_scores.sash_id = ?', id)
32
- if (category = options[:category])
33
- scope = scope.where('merit_scores.category = ?', category)
34
- end
35
- scope
36
- end
37
-
38
34
  def add_points(num_points, options = {})
39
35
  point = Merit::Score::Point.new
40
36
  point.num_points = num_points
@@ -8,6 +8,7 @@ module Merit
8
8
 
9
9
  attr_accessible :badge_id if show_attr_accessible?
10
10
 
11
+ belongs_to :sash, class_name: 'Merit::Sash'
11
12
  has_many :activity_logs, class_name: 'Merit::ActivityLog', as: :related_change
12
13
 
13
14
  def self.last_granted(options = {})
@@ -15,14 +15,16 @@ module Merit
15
15
 
16
16
  after_create :create_scores
17
17
 
18
- def add_badge(badge_id)
19
- bs = Merit::BadgesSash.new(badge_id: badge_id)
20
- badges_sashes.push(bs)
21
- end
22
-
23
- def rm_badge(badge_id)
24
- bs = badges_sashes.where(badge_id: badge_id).first
25
- badges_sashes.delete(bs)
18
+ # Retrieve all points from a category or none if category doesn't exist
19
+ # By default retrieve all Points
20
+ # @param category [String] The category
21
+ # @return [ActiveRecord::Relation] containing the points
22
+ def score_points(options = {})
23
+ scope = scores
24
+ if (category = options[:category])
25
+ scope = scope.where(category: category)
26
+ end
27
+ Merit::Score::Point.where(:score_id.in => scope.map(&:_id))
26
28
  end
27
29
  end
28
30
  end
@@ -5,7 +5,7 @@ module Merit
5
5
 
6
6
  field :category, type: String, default: 'default'
7
7
 
8
- belongs_to :sash
8
+ belongs_to :sash, class_name: 'Merit::Sash'
9
9
  has_many :score_points, class_name: 'Merit::Score::Point', dependent: :destroy
10
10
 
11
11
  # Meant to display a leaderboard. Accepts options :table_name (users by
@@ -36,6 +36,10 @@ module Merit
36
36
 
37
37
  belongs_to :score, class_name: 'Merit::Score'
38
38
  has_many :activity_logs, class_name: 'Merit::ActivityLog', as: :related_change
39
+
40
+ def sash_id
41
+ score.sash_id
42
+ end
39
43
  end
40
44
  end
41
45
  end
@@ -1,9 +1,11 @@
1
1
  module Merit
2
2
  class ReputationChangeObserver
3
3
  def update(changed_data)
4
+ # TODO: sometimes we recieved true in changed_data[:merit_object]
5
+ # it should be nil or merit object with activity_logs relation
4
6
  ActivityLog.create(
5
7
  description: changed_data[:description],
6
- related_change: changed_data[:merit_object],
8
+ related_change: (changed_data[:merit_object] if changed_data[:merit_object].respond_to?(:activity_logs)),
7
9
  action_id: changed_data[:merit_action_id]
8
10
  )
9
11
  end
data/lib/merit.rb CHANGED
@@ -72,10 +72,10 @@ module Merit
72
72
 
73
73
  class Engine < Rails::Engine
74
74
  config.app_generators.orm Merit.orm
75
- config.eager_load_paths << File.expand_path("../merit/models/#{Merit.orm}", __FILE__)
76
75
 
77
76
  initializer 'merit.controller' do |app|
78
77
  extend_orm_with_has_merit
78
+ require_models
79
79
  ActiveSupport.on_load(:action_controller) do
80
80
  begin
81
81
  # Load app rules on boot up
@@ -89,6 +89,15 @@ module Merit
89
89
  end
90
90
  end
91
91
 
92
+ def require_models
93
+ require 'merit/models/base/sash'
94
+ require 'merit/models/base/badges_sash'
95
+ require "merit/models/#{Merit.orm}/merit/activity_log"
96
+ require "merit/models/#{Merit.orm}/merit/badges_sash"
97
+ require "merit/models/#{Merit.orm}/merit/sash"
98
+ require "merit/models/#{Merit.orm}/merit/score"
99
+ end
100
+
92
101
  def extend_orm_with_has_merit
93
102
  if Object.const_defined?('ActiveRecord')
94
103
  ActiveRecord::Base.send :include, Merit
data/merit.gemspec CHANGED
@@ -5,7 +5,7 @@ Gem::Specification.new do |s|
5
5
  s.homepage = "http://github.com/tute/merit"
6
6
  s.files = `git ls-files`.split("\n").reject{|f| f =~ /^\./ }
7
7
  s.license = 'MIT'
8
- s.version = '2.0.0'
8
+ s.version = '2.1.0'
9
9
  s.authors = ["Tute Costa"]
10
10
  s.email = 'tutecosta@gmail.com'
11
11
 
@@ -13,6 +13,7 @@ Gem::Specification.new do |s|
13
13
 
14
14
  s.add_dependency 'ambry', '~> 0.3.0'
15
15
  s.add_development_dependency 'rails', '>= 3.2.0'
16
+ s.add_development_dependency 'jquery-rails', '~> 2.1'
16
17
  s.add_development_dependency 'capybara'
17
18
  s.add_development_dependency 'simplecov'
18
19
  s.add_development_dependency 'rubocop'
@@ -1,6 +1,9 @@
1
1
  case Merit.orm
2
2
  when :active_record
3
3
  class Comment < ActiveRecord::Base
4
+ def friend
5
+ User.find_by_name('friend')
6
+ end
4
7
  end
5
8
  when :mongoid
6
9
  class Comment
@@ -9,7 +12,11 @@ when :mongoid
9
12
 
10
13
  field :name, :type => String
11
14
  field :comment, :type => String
12
- field :votes, :type => Integer
15
+ field :votes, :type => Integer, :default => 0
16
+
17
+ def friend
18
+ User.find_by(name: 'friend')
19
+ end
13
20
  end
14
21
  end
15
22
 
@@ -25,8 +32,4 @@ class Comment
25
32
  validates :name, :comment, :user_id, :presence => true
26
33
 
27
34
  delegate :comments, :to => :user, :prefix => true
28
-
29
- def friend
30
- User.find_by_name('friend')
31
- end
32
35
  end
@@ -4,6 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <title>Merit Dummy app</title>
6
6
  <%= stylesheet_link_tag :all %>
7
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
8
+ <script src="/rails.js"></script>
7
9
  <%= csrf_meta_tag %>
8
10
  </head>
9
11
  <body>
@@ -30,5 +30,5 @@ Dummy::Application.configure do
30
30
  # Print deprecation notices to the stderr
31
31
  config.active_support.deprecation = :stderr
32
32
 
33
- config.eager_load = false
33
+ config.eager_load = ENV['RAILS_VERSION'] == '4.0' || ENV['RAILS_VERSION'] == '4.0-protected-attributes'
34
34
  end
@@ -1,13 +1,13 @@
1
1
  development:
2
2
  sessions:
3
3
  default:
4
- database: mongoid_dev
4
+ database: merit_dev
5
5
  hosts:
6
6
  - localhost:27017
7
7
 
8
8
  test:
9
9
  sessions:
10
10
  default:
11
- database: mongoid_test
11
+ database: merit_test
12
12
  hosts:
13
13
  - localhost:27017
@@ -10,7 +10,7 @@ Dummy::Application.routes.draw do
10
10
  resources :registrations, :only => :update, :as => :registrations_user
11
11
  resources :comments
12
12
 
13
- get '/comments/:id/vote/:value' => 'comments#vote', :id => /\d+/, :value => /\d+/
13
+ get '/comments/:id/vote/:value' => 'comments#vote', :value => /\d+/
14
14
 
15
15
  root :to => 'users#index'
16
16
  end
@@ -0,0 +1,404 @@
1
+ (function($, undefined) {
2
+
3
+ /**
4
+ * Unobtrusive scripting adapter for jQuery
5
+ * https://github.com/rails/jquery-ujs
6
+ *
7
+ * Requires jQuery 1.7.0 or later.
8
+ *
9
+ * Released under the MIT license
10
+ *
11
+ */
12
+
13
+ // Cut down on the number of issues from people inadvertently including jquery_ujs twice
14
+ // by detecting and raising an error when it happens.
15
+ if ( $.rails !== undefined ) {
16
+ $.error('jquery-ujs has already been loaded!');
17
+ }
18
+
19
+ // Shorthand to make it a little easier to call public rails functions from within rails.js
20
+ var rails;
21
+ var $document = $(document);
22
+
23
+ $.rails = rails = {
24
+ // Link elements bound by jquery-ujs
25
+ linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]',
26
+
27
+ // Button elements bound by jquery-ujs
28
+ buttonClickSelector: 'button[data-remote], button[data-confirm]',
29
+
30
+ // Select elements bound by jquery-ujs
31
+ inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]',
32
+
33
+ // Form elements bound by jquery-ujs
34
+ formSubmitSelector: 'form',
35
+
36
+ // Form input elements bound by jquery-ujs
37
+ formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])',
38
+
39
+ // Form input elements disabled during form submission
40
+ disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]',
41
+
42
+ // Form input elements re-enabled after form submission
43
+ enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled',
44
+
45
+ // Form required input elements
46
+ requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])',
47
+
48
+ // Form file input elements
49
+ fileInputSelector: 'input[type=file]',
50
+
51
+ // Link onClick disable selector with possible reenable after remote submission
52
+ linkDisableSelector: 'a[data-disable-with]',
53
+
54
+ // Make sure that every Ajax request sends the CSRF token
55
+ CSRFProtection: function(xhr) {
56
+ var token = $('meta[name="csrf-token"]').attr('content');
57
+ if (token) xhr.setRequestHeader('X-CSRF-Token', token);
58
+ },
59
+
60
+ // making sure that all forms have actual up-to-date token(cached forms contain old one)
61
+ refreshCSRFTokens: function(){
62
+ var csrfToken = $('meta[name=csrf-token]').attr('content');
63
+ var csrfParam = $('meta[name=csrf-param]').attr('content');
64
+ $('form input[name="' + csrfParam + '"]').val(csrfToken);
65
+ },
66
+
67
+ // Triggers an event on an element and returns false if the event result is false
68
+ fire: function(obj, name, data) {
69
+ var event = $.Event(name);
70
+ obj.trigger(event, data);
71
+ return event.result !== false;
72
+ },
73
+
74
+ // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm
75
+ confirm: function(message) {
76
+ return confirm(message);
77
+ },
78
+
79
+ // Default ajax function, may be overridden with custom function in $.rails.ajax
80
+ ajax: function(options) {
81
+ return $.ajax(options);
82
+ },
83
+
84
+ // Default way to get an element's href. May be overridden at $.rails.href.
85
+ href: function(element) {
86
+ return element.attr('href');
87
+ },
88
+
89
+ // Submits "remote" forms and links with ajax
90
+ handleRemote: function(element) {
91
+ var method, url, data, elCrossDomain, crossDomain, withCredentials, dataType, options;
92
+
93
+ if (rails.fire(element, 'ajax:before')) {
94
+ elCrossDomain = element.data('cross-domain');
95
+ crossDomain = elCrossDomain === undefined ? null : elCrossDomain;
96
+ withCredentials = element.data('with-credentials') || null;
97
+ dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType);
98
+
99
+ if (element.is('form')) {
100
+ method = element.attr('method');
101
+ url = element.attr('action');
102
+ data = element.serializeArray();
103
+ // memoized value from clicked submit button
104
+ var button = element.data('ujs:submit-button');
105
+ if (button) {
106
+ data.push(button);
107
+ element.data('ujs:submit-button', null);
108
+ }
109
+ } else if (element.is(rails.inputChangeSelector)) {
110
+ method = element.data('method');
111
+ url = element.data('url');
112
+ data = element.serialize();
113
+ if (element.data('params')) data = data + "&" + element.data('params');
114
+ } else if (element.is(rails.buttonClickSelector)) {
115
+ method = element.data('method') || 'get';
116
+ url = element.data('url');
117
+ data = element.serialize();
118
+ if (element.data('params')) data = data + "&" + element.data('params');
119
+ } else {
120
+ method = element.data('method');
121
+ url = rails.href(element);
122
+ data = element.data('params') || null;
123
+ }
124
+
125
+ options = {
126
+ type: method || 'GET', data: data, dataType: dataType,
127
+ // stopping the "ajax:beforeSend" event will cancel the ajax request
128
+ beforeSend: function(xhr, settings) {
129
+ if (settings.dataType === undefined) {
130
+ xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
131
+ }
132
+ if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) {
133
+ element.trigger('ajax:send', xhr);
134
+ } else {
135
+ return false;
136
+ }
137
+ },
138
+ success: function(data, status, xhr) {
139
+ element.trigger('ajax:success', [data, status, xhr]);
140
+ },
141
+ complete: function(xhr, status) {
142
+ element.trigger('ajax:complete', [xhr, status]);
143
+ },
144
+ error: function(xhr, status, error) {
145
+ element.trigger('ajax:error', [xhr, status, error]);
146
+ },
147
+ crossDomain: crossDomain
148
+ };
149
+
150
+ // There is no withCredentials for IE6-8 when
151
+ // "Enable native XMLHTTP support" is disabled
152
+ if (withCredentials) {
153
+ options.xhrFields = {
154
+ withCredentials: withCredentials
155
+ };
156
+ }
157
+
158
+ // Only pass url to `ajax` options if not blank
159
+ if (url) { options.url = url; }
160
+
161
+ return rails.ajax(options);
162
+ } else {
163
+ return false;
164
+ }
165
+ },
166
+
167
+ // Handles "data-method" on links such as:
168
+ // <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
169
+ handleMethod: function(link) {
170
+ var href = rails.href(link),
171
+ method = link.data('method'),
172
+ target = link.attr('target'),
173
+ csrfToken = $('meta[name=csrf-token]').attr('content'),
174
+ csrfParam = $('meta[name=csrf-param]').attr('content'),
175
+ form = $('<form method="post" action="' + href + '"></form>'),
176
+ metadataInput = '<input name="_method" value="' + method + '" type="hidden" />';
177
+
178
+ if (csrfParam !== undefined && csrfToken !== undefined) {
179
+ metadataInput += '<input name="' + csrfParam + '" value="' + csrfToken + '" type="hidden" />';
180
+ }
181
+
182
+ if (target) { form.attr('target', target); }
183
+
184
+ form.hide().append(metadataInput).appendTo('body');
185
+ form.submit();
186
+ },
187
+
188
+ /* Disables form elements:
189
+ - Caches element value in 'ujs:enable-with' data store
190
+ - Replaces element text with value of 'data-disable-with' attribute
191
+ - Sets disabled property to true
192
+ */
193
+ disableFormElements: function(form) {
194
+ form.find(rails.disableSelector).each(function() {
195
+ var element = $(this), method = element.is('button') ? 'html' : 'val';
196
+ element.data('ujs:enable-with', element[method]());
197
+ element[method](element.data('disable-with'));
198
+ element.prop('disabled', true);
199
+ });
200
+ },
201
+
202
+ /* Re-enables disabled form elements:
203
+ - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`)
204
+ - Sets disabled property to false
205
+ */
206
+ enableFormElements: function(form) {
207
+ form.find(rails.enableSelector).each(function() {
208
+ var element = $(this), method = element.is('button') ? 'html' : 'val';
209
+ if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with'));
210
+ element.prop('disabled', false);
211
+ });
212
+ },
213
+
214
+ /* For 'data-confirm' attribute:
215
+ - Fires `confirm` event
216
+ - Shows the confirmation dialog
217
+ - Fires the `confirm:complete` event
218
+
219
+ Returns `true` if no function stops the chain and user chose yes; `false` otherwise.
220
+ Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog.
221
+ Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function
222
+ return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog.
223
+ */
224
+ allowAction: function(element) {
225
+ var message = element.data('confirm'),
226
+ answer = false, callback;
227
+ if (!message) { return true; }
228
+
229
+ if (rails.fire(element, 'confirm')) {
230
+ answer = rails.confirm(message);
231
+ callback = rails.fire(element, 'confirm:complete', [answer]);
232
+ }
233
+ return answer && callback;
234
+ },
235
+
236
+ // Helper function which checks for blank inputs in a form that match the specified CSS selector
237
+ blankInputs: function(form, specifiedSelector, nonBlank) {
238
+ var inputs = $(), input, valueToCheck,
239
+ selector = specifiedSelector || 'input,textarea',
240
+ allInputs = form.find(selector);
241
+
242
+ allInputs.each(function() {
243
+ input = $(this);
244
+ valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : input.val();
245
+ // If nonBlank and valueToCheck are both truthy, or nonBlank and valueToCheck are both falsey
246
+ if (!valueToCheck === !nonBlank) {
247
+
248
+ // Don't count unchecked required radio if other radio with same name is checked
249
+ if (input.is('input[type=radio]') && allInputs.filter('input[type=radio]:checked[name="' + input.attr('name') + '"]').length) {
250
+ return true; // Skip to next input
251
+ }
252
+
253
+ inputs = inputs.add(input);
254
+ }
255
+ });
256
+ return inputs.length ? inputs : false;
257
+ },
258
+
259
+ // Helper function which checks for non-blank inputs in a form that match the specified CSS selector
260
+ nonBlankInputs: function(form, specifiedSelector) {
261
+ return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank
262
+ },
263
+
264
+ // Helper function, needed to provide consistent behavior in IE
265
+ stopEverything: function(e) {
266
+ $(e.target).trigger('ujs:everythingStopped');
267
+ e.stopImmediatePropagation();
268
+ return false;
269
+ },
270
+
271
+ // replace element's html with the 'data-disable-with' after storing original html
272
+ // and prevent clicking on it
273
+ disableElement: function(element) {
274
+ element.data('ujs:enable-with', element.html()); // store enabled state
275
+ element.html(element.data('disable-with')); // set to disabled state
276
+ element.bind('click.railsDisable', function(e) { // prevent further clicking
277
+ return rails.stopEverything(e);
278
+ });
279
+ },
280
+
281
+ // restore element to its original state which was disabled by 'disableElement' above
282
+ enableElement: function(element) {
283
+ if (element.data('ujs:enable-with') !== undefined) {
284
+ element.html(element.data('ujs:enable-with')); // set to old enabled state
285
+ element.removeData('ujs:enable-with'); // clean up cache
286
+ }
287
+ element.unbind('click.railsDisable'); // enable element
288
+ }
289
+
290
+ };
291
+
292
+ if (rails.fire($document, 'rails:attachBindings')) {
293
+
294
+ $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }});
295
+
296
+ $document.delegate(rails.linkDisableSelector, 'ajax:complete', function() {
297
+ rails.enableElement($(this));
298
+ });
299
+
300
+ $document.delegate(rails.linkClickSelector, 'click.rails', function(e) {
301
+ var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey;
302
+ if (!rails.allowAction(link)) return rails.stopEverything(e);
303
+
304
+ if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link);
305
+
306
+ if (link.data('remote') !== undefined) {
307
+ if (metaClick && (!method || method === 'GET') && !data) { return true; }
308
+
309
+ var handleRemote = rails.handleRemote(link);
310
+ // response from rails.handleRemote() will either be false or a deferred object promise.
311
+ if (handleRemote === false) {
312
+ rails.enableElement(link);
313
+ } else {
314
+ handleRemote.error( function() { rails.enableElement(link); } );
315
+ }
316
+ return false;
317
+
318
+ } else if (link.data('method')) {
319
+ rails.handleMethod(link);
320
+ return false;
321
+ }
322
+ });
323
+
324
+ $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) {
325
+ var button = $(this);
326
+ if (!rails.allowAction(button)) return rails.stopEverything(e);
327
+
328
+ rails.handleRemote(button);
329
+ return false;
330
+ });
331
+
332
+ $document.delegate(rails.inputChangeSelector, 'change.rails', function(e) {
333
+ var link = $(this);
334
+ if (!rails.allowAction(link)) return rails.stopEverything(e);
335
+
336
+ rails.handleRemote(link);
337
+ return false;
338
+ });
339
+
340
+ $document.delegate(rails.formSubmitSelector, 'submit.rails', function(e) {
341
+ var form = $(this),
342
+ remote = form.data('remote') !== undefined,
343
+ blankRequiredInputs,
344
+ nonBlankFileInputs;
345
+
346
+ if (!rails.allowAction(form)) return rails.stopEverything(e);
347
+
348
+ // skip other logic when required values are missing or file upload is present
349
+ if (form.attr('novalidate') == undefined) {
350
+ blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector);
351
+ if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
352
+ return rails.stopEverything(e);
353
+ }
354
+ }
355
+
356
+ if (remote) {
357
+ nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
358
+ if (nonBlankFileInputs) {
359
+ // slight timeout so that the submit button gets properly serialized
360
+ // (make it easy for event handler to serialize form without disabled values)
361
+ setTimeout(function(){ rails.disableFormElements(form); }, 13);
362
+ var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
363
+
364
+ // re-enable form elements if event bindings return false (canceling normal form submission)
365
+ if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); }
366
+
367
+ return aborted;
368
+ }
369
+
370
+ rails.handleRemote(form);
371
+ return false;
372
+
373
+ } else {
374
+ // slight timeout so that the submit button gets properly serialized
375
+ setTimeout(function(){ rails.disableFormElements(form); }, 13);
376
+ }
377
+ });
378
+
379
+ $document.delegate(rails.formInputClickSelector, 'click.rails', function(event) {
380
+ var button = $(this);
381
+
382
+ if (!rails.allowAction(button)) return rails.stopEverything(event);
383
+
384
+ // register the pressed submit button
385
+ var name = button.attr('name'),
386
+ data = name ? {name:name, value:button.val()} : null;
387
+
388
+ button.closest('form').data('ujs:submit-button', data);
389
+ });
390
+
391
+ $document.delegate(rails.formSubmitSelector, 'ajax:send.rails', function(event) {
392
+ if (this == event.target) rails.disableFormElements($(this));
393
+ });
394
+
395
+ $document.delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) {
396
+ if (this == event.target) rails.enableFormElements($(this));
397
+ });
398
+
399
+ $(function(){
400
+ rails.refreshCSRFTokens();
401
+ });
402
+ }
403
+
404
+ })( jQuery );
@@ -1,6 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class NavigationTest < ActiveSupport::IntegrationCase
4
+
4
5
  def tear_down
5
6
  DummyObserver.unstub(:update)
6
7
  end
@@ -91,8 +92,17 @@ class NavigationTest < ActiveSupport::IntegrationCase
91
92
  assert_equal 0, user.points
92
93
  assert_equal 2, Merit::Score::Point.count
93
94
 
94
- # Make tenth comment, assert 10-commenter badge granted
95
+ # Tenth comment with errors doesn't change reputation
96
+ badges = user.reload.badges
97
+ points = user.points
95
98
  visit '/comments/new'
99
+ assert_no_difference('Merit::ActivityLog.count') do
100
+ click_button('Create Comment')
101
+ end
102
+ assert_equal badges, user.reload.badges
103
+ assert_equal points, user.points
104
+
105
+ # Tenth comment without errors, assert 10-commenter badge granted
96
106
  fill_in 'Name', with: 'Hi!'
97
107
  fill_in 'Comment', with: 'Hi bro!'
98
108
  fill_in 'User', with: user.id
@@ -0,0 +1,2 @@
1
+ # Place orm-dependent test preparation here
2
+ ActiveRecord::Migrator.migrate File.expand_path("../../dummy/db/migrate/", __FILE__)
@@ -0,0 +1,6 @@
1
+ # Place orm-dependent test preparation here
2
+ class ActiveSupport::TestCase
3
+ setup do
4
+ Mongoid.purge!
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class User < ActiveRecord::Base
4
+ has_merit
5
+ end
6
+
7
+ class Fruit < ActiveRecord::Base
8
+ end
9
+
10
+ class Soldier < ActiveRecord::Base
11
+ has_merit
12
+ end
13
+ class Player < ActiveRecord::Base
14
+ has_merit
15
+ end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ class User
4
+ include Mongoid::Document
5
+ has_merit
6
+ end
7
+
8
+ class Fruit
9
+ include Mongoid::Document
10
+ end
11
+
12
+ class Soldier
13
+ include Mongoid::Document
14
+ has_merit
15
+ end
16
+
17
+ class Player
18
+ include Mongoid::Document
19
+ has_merit
20
+ end
data/test/test_helper.rb CHANGED
@@ -2,6 +2,9 @@
2
2
  ENV['RAILS_ENV'] = 'test'
3
3
  RUBYOPT="-w $RUBYOPT"
4
4
 
5
+ require 'coveralls'
6
+ Coveralls.wear!('rails')
7
+
5
8
  if ENV["COVERAGE"]
6
9
  require 'simplecov'
7
10
  SimpleCov.adapters.define 'rubygem' do
@@ -26,12 +29,17 @@ require 'capybara/rails'
26
29
  Capybara.default_driver = :rack_test
27
30
  Capybara.default_selector = :css
28
31
 
29
- ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
32
+ Merit.orm = :active_record if Merit.orm.nil?
33
+
34
+ def active_record_orm?
35
+ Merit.orm == :active_record
36
+ end
37
+
38
+ def mongoid_orm?
39
+ Merit.orm == :mongoid
40
+ end
30
41
 
31
- require "merit/models/#{Merit.orm}/merit/activity_log"
32
- require "merit/models/#{Merit.orm}/merit/badges_sash"
33
- require "merit/models/#{Merit.orm}/merit/sash"
34
- require "merit/models/#{Merit.orm}/merit/score"
42
+ require "orm/#{Merit.orm}"
35
43
 
36
44
  # Load support files
37
45
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
@@ -2,24 +2,14 @@ require 'test_helper'
2
2
 
3
3
  # TODO: Split different objects tests in it's own files
4
4
  class MeritUnitTest < ActiveSupport::TestCase
5
- test 'extends only meritable ActiveRecord models' do
6
- class User < ActiveRecord::Base
7
- has_merit
8
- end
9
- class Fruit < ActiveRecord::Base
10
- end
5
+ require "orm_models/#{Merit.orm}"
11
6
 
7
+ test 'extends only meritable models' do
12
8
  assert User.method_defined?(:points), 'has_merit adds methods'
13
9
  assert !Fruit.method_defined?(:points), 'other models aren\'t extended'
14
10
  end
15
11
 
16
12
  test 'Badges get "related_models" methods' do
17
- class Soldier < ActiveRecord::Base
18
- has_merit
19
- end
20
- class Player < ActiveRecord::Base
21
- has_merit
22
- end
23
13
  assert Merit::Badge.method_defined?(:soldiers), 'Badge#soldiers should be defined'
24
14
  assert Merit::Badge.method_defined?(:players), 'Badge#players should be defined'
25
15
  end
@@ -34,7 +24,7 @@ class MeritUnitTest < ActiveSupport::TestCase
34
24
  assert_raises Merit::RankAttributeNotDefined do
35
25
  WeirdRankRules.new.check_rank_rules
36
26
  end
37
- end
27
+ end if active_record_orm?
38
28
 
39
29
  test 'Badge#custom_fields_hash saves correctly' do
40
30
  Merit::Badge.create(id: 99,
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ describe Merit::Score do
4
+ it 'Point#sash_id delegates to Score' do
5
+ score = mock()
6
+ score.stubs(:sash_id).returns(33)
7
+
8
+ point = Merit::Score::Point.new
9
+ point.stubs(:score).returns(score)
10
+
11
+ point.sash_id.must_be :==, score.sash_id
12
+ end
13
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: merit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tute Costa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-22 00:00:00.000000000 Z
11
+ date: 2014-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ambry
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 3.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: jquery-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: capybara
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -235,10 +249,15 @@ files:
235
249
  - test/dummy/public/javascripts/effects.js
236
250
  - test/dummy/public/javascripts/prototype.js
237
251
  - test/dummy/public/javascripts/rails.js
252
+ - test/dummy/public/rails.js
238
253
  - test/dummy/public/stylesheets/.gitkeep
239
254
  - test/dummy/public/stylesheets/scaffold.css
240
255
  - test/dummy/script/rails
241
256
  - test/integration/navigation_test.rb
257
+ - test/orm/active_record.rb
258
+ - test/orm/mongoid.rb
259
+ - test/orm_models/active_record.rb
260
+ - test/orm_models/mongoid.rb
242
261
  - test/support/integration_case.rb
243
262
  - test/test_helper.rb
244
263
  - test/unit/base_target_finder_test.rb
@@ -247,6 +266,7 @@ files:
247
266
  - test/unit/rules_matcher_test.rb
248
267
  - test/unit/sash_finder_test.rb
249
268
  - test/unit/sash_test.rb
269
+ - test/unit/score_test.rb
250
270
  - test/unit/target_finder_test.rb
251
271
  homepage: http://github.com/tute/merit
252
272
  licenses: