merit 2.0.0 → 2.1.0

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