paginative 0.2.3 → 0.3.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: d5cef3e31235b6ee8b8e8bd64f29f9702be0e2a0
4
- data.tar.gz: 3624c839c406848e43290ced9f17d09c7ac58f0e
3
+ metadata.gz: b88a52a9b7c22d3a811c3e011020e3b7ea1c624a
4
+ data.tar.gz: c5b8c8e2f43df8fed6cc57502afc717cad2944eb
5
5
  SHA512:
6
- metadata.gz: 6e4c301c39d85570c897fee526235594f2ea1e46d0c5f54c5e3911fc6fd3dfeb0a178968ce6b4cf581e06aa6681841f615f7b2ff9e326b6c5cff4a52c11df32f
7
- data.tar.gz: 7c136f4ee68fe21d7b1181db71832e7eb0d205d7f6614ee716065caddb69279c1f75388c4bf39a50a0ca5943b0dae0fa23d2835c8d9be8cc9992c9c483994351
6
+ metadata.gz: f0f7ac4dcf60b1741760d3ba714ed7d9ae2931c46a8b843e89655fbd359ed889329a9818e7026aa7d575c15a0931e95f618c23f8e8ba411fae21ecb56616eaa0
7
+ data.tar.gz: c0a047e197c29d41642368da7cd4dd12de36db57e412c8d5969bb5278277dae2b7f713a590268c87b405cc85ef66660ca4fd1d3e651dc6ee615ddc9370940575
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  begin
2
+ require 'rubygems'
2
3
  require 'bundler/setup'
3
4
  rescue LoadError
4
5
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
@@ -5,6 +5,9 @@ module Paginative
5
5
  included do
6
6
  include Paginative::OrderingHelpers
7
7
 
8
+ mattr_accessor :paginative_fields
9
+ @@paginative_fields = {}
10
+
8
11
  def self.by_distance_from(latitude, longitude, distance=0, limit=25)
9
12
  return [] unless latitude.present? && longitude.present?
10
13
  distance_sql = send(:distance_sql, latitude.to_f, longitude.to_f, {:units => :km, select_bearing: false})
@@ -23,24 +26,98 @@ module Paginative
23
26
 
24
27
  def self.with_field_from(field="", value="", limit=25, order="asc")
25
28
  order ||= "asc"
26
- if field.is_a? Array
27
- return raise "Wrong number of values. Expected 2, got #{value.try(:length)}. You must pass a value for each field that you are sorting by" unless value.is_a?(Array) && value.length == 2
28
- # You can now pass in an array of 'field' params so that you can have a secondary sort order.
29
- # This is important if your primary sort field could have duplicate values
30
- primary_sort_field = field[0]
31
- primary_value = value[0]
32
- secondary_sort_field = field[1]
33
- secondary_value = value[1]
34
- # This allows us to pass in 2 different sort columns and still paginate correctly.
35
- return self.order(sanitized_ordering(self.table_name, field, order)).where("#{self.table_name}.#{primary_sort_field} <= ? AND (#{self.table_name}.#{primary_sort_field} != ? OR #{self.table_name}.#{secondary_sort_field} < ?)", primary_value, primary_value, secondary_value) if order.try(:downcase) == "desc"
36
-
37
- self.order(sanitized_ordering(self.table_name, field, order)).where("#{self.table_name}.#{primary_sort_field} >= ? AND (#{self.table_name}.#{primary_sort_field} != ? OR #{self.table_name}.#{secondary_sort_field} > ?)", primary_value, primary_value, secondary_value)
29
+
30
+ # Wrap and flatten whatever comes in, if it's a single value, we end up with an array.
31
+ # If it's an array, stays an array.
32
+ fields = Array.wrap(field).flatten
33
+ values = Array.wrap(value).flatten
34
+ zipped = fields.zip(values)
35
+ fields, values = prune_fields(zipped).transpose
36
+
37
+ q = self.all
38
+ if fields.present? && fields.any?
39
+ return raise "Wrong number of values. Expected 2, got #{values.try(:length)}. You must pass a value for each field that you are sorting by" unless values.length <= 2 && values.length == fields.length
40
+
41
+ mapped_fields = map_fields(fields)
42
+ q = q.order(sanitized_ordering(self.table_name, mapped_fields, order))
43
+
44
+ mapped_fields.each_with_index do |field, idx|
45
+ if idx == 0
46
+ value = values[idx]
47
+ operator = sort_operator(idx, mapped_fields.count, order)
48
+
49
+ q = q.where("#{field} #{operator} ?", value)
50
+ else
51
+ previous_field = mapped_fields[idx - 1]
52
+ previous_value = values[idx - 1]
53
+ value = values[idx]
54
+ operator = sort_operator(idx, mapped_fields.count, order)
55
+
56
+ q = q.where("#{previous_field} != ? OR #{field} #{operator} ?", previous_value, value)
57
+ end
58
+ end
59
+ end
60
+
61
+ return q.limit(limit)
62
+ end
63
+
64
+ private
65
+
66
+ # Steps through the provided paginated fields, zipped with their values, and removes those not
67
+ # in the `paginative_fields` hash as specified with `allow_paginative_on`.
68
+ def self.prune_fields(zipped)
69
+ zipped.select{ |f, v| self.paginative_fields.has_key? f.to_sym }.tap do |pruned|
70
+ unless pruned.nil?
71
+ items = zipped.map(&:first) - pruned.map(&:first)
72
+ Rails.logger.warn "Paginative ignored unpermitted field: #{items}"
73
+ end
74
+ end
75
+ end
76
+
77
+ # Takes the pruned fields as an array, and returns the mapped versions.
78
+ def self.map_fields(fields)
79
+ fields.map{ |f| self.paginative_fields[f.to_sym] }
80
+ end
81
+
82
+ # Returns the appropriate order given sort direction and current field in the collection.
83
+ # We don't want to use inclusive paging if we are at the last field being paginated, so either lt or gt is used.
84
+ def self.sort_operator(index, count, direction)
85
+ if direction.try(:downcase) == "desc"
86
+ index < (count - 1) ? '<=' : '<'
87
+ else
88
+ index < (count - 1) ? '>=' : '>'
89
+ end
90
+ end
91
+ end
92
+
93
+ module ClassMethods
94
+ # Sets the paginative fields set of the class to the specified columns.
95
+ def allow_paginative_on(*mappings)
96
+ self.paginative_fields = process_fields(mappings)
97
+ end
98
+
99
+ private
100
+
101
+ # Process specified mappings to either scope to the table name of the current class
102
+ # or to use the mappings provided.
103
+ def process_fields(mappings)
104
+ result = {}
105
+
106
+ mappings.each do |mapping|
107
+ if mapping.is_a?(Hash)
108
+ result.merge!(mapping)
38
109
  else
39
- value = value.to_i if self.column_for_attribute(field).type == :integer
40
- return self.order(sanitized_ordering(self.table_name, field, order)).where("#{self.table_name}.#{field} < ?", value).limit(limit) if order.try(:downcase) == "desc"
41
- self.order(sanitized_ordering(self.table_name, field, order)).where("#{self.table_name}.#{field} > ?", value).limit(limit)
110
+ result[mapping] = self_map(mapping)
42
111
  end
43
112
  end
113
+
114
+ result
115
+ end
116
+
117
+ # Returns a string scoping the specified field to the current class' table.
118
+ def self_map(field)
119
+ "#{self.table_name}.#{field}"
44
120
  end
45
121
  end
46
122
  end
123
+ end
@@ -3,20 +3,13 @@ module Paginative
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- def self.sanitized_ordering(table_name, field, order)
7
- if field.is_a? Array
8
- return raise "Wrong number of sorting fields. Expected 2, got #{field.length}. If you want to sort by a singular field please pass field argument as a string rather than an array." unless field.length == 2
9
-
10
- "#{table_name}.#{sanitize_column(field[0])} #{sanitize_column_direction(order)}, #{table_name}.#{sanitize_column(field[1])} #{sanitize_column_direction(order)}"
11
- else
12
- "#{table_name}.#{sanitize_column(field)} #{sanitize_column_direction(order)}"
13
- end
6
+ def self.sanitized_ordering(table_name, fields, order)
7
+ fields.map do |field|
8
+ "#{field} #{sanitize_column_direction(order)}"
9
+ end.join(', ')
14
10
  end
15
11
 
16
12
  private
17
- def self.sanitize_column(column)
18
- self.column_names.include?(column) ? column : "created_at"
19
- end
20
13
 
21
14
  def self.sanitize_column_direction(direction)
22
15
  direction = direction.upcase
@@ -1,3 +1,3 @@
1
1
  module Paginative
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,2 @@
1
+ class JointModel < ActiveRecord::Base
2
+ end
@@ -2,4 +2,14 @@ class TestModel < ActiveRecord::Base
2
2
  include Paginative::ModelExtension
3
3
 
4
4
  reverse_geocoded_by :latitude, :longitude
5
+
6
+ # Associations
7
+ has_many :joint_models
8
+
9
+ class << self
10
+ def joint
11
+ joins(:joint_models).select('test_models.*, joint_models.created_at')
12
+ .order('joint_models.created_at DESC')
13
+ end
14
+ end
5
15
  end
@@ -0,0 +1,10 @@
1
+ class CreateJointModels < ActiveRecord::Migration
2
+ def change
3
+ create_table :joint_models do |t|
4
+ t.string :name
5
+ t.references :test_model, index: true
6
+
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -9,17 +9,26 @@
9
9
  # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
10
  # you'll amass, the slower it'll run and the greater likelihood for issues).
11
11
  #
12
- # It's strongly recommended to check this file into your version control system.
12
+ # It's strongly recommended that you check this file into your version control system.
13
13
 
14
- ActiveRecord::Schema.define(:version => 20140416035443) do
14
+ ActiveRecord::Schema.define(version: 20150922072155) do
15
15
 
16
- create_table "test_models", :force => true do |t|
16
+ create_table "joint_models", force: :cascade do |t|
17
+ t.string "name"
18
+ t.integer "test_model_id"
19
+ t.datetime "created_at", null: false
20
+ t.datetime "updated_at", null: false
21
+ end
22
+
23
+ add_index "joint_models", ["test_model_id"], name: "index_joint_models_on_test_model_id"
24
+
25
+ create_table "test_models", force: :cascade do |t|
17
26
  t.string "name"
18
27
  t.string "address"
19
28
  t.float "latitude"
20
29
  t.float "longitude"
21
- t.datetime "created_at", :null => false
22
- t.datetime "updated_at", :null => false
30
+ t.datetime "created_at"
31
+ t.datetime "updated_at"
23
32
  end
24
33
 
25
34
  end
@@ -0,0 +1,6 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :joint_model do
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # Read about factories at https://github.com/thoughtbot/factory_girl
2
+
3
+ FactoryGirl.define do
4
+ factory :joint_model, class: 'JointModel' do
5
+ sequence(:name) { |n| "#{("a".."zzz").to_a[n]}" }
6
+ end
7
+ end
@@ -4,10 +4,7 @@ describe TestModel do
4
4
 
5
5
  before :each do
6
6
  TestModel.class_eval do
7
- include Paginative::ModelExtension
8
-
9
- reverse_geocoded_by :latitude, :longitude
10
- after_validation :reverse_geocode # auto-fetch coordinates
7
+ allow_paginative_on :id, :latitude, :longitude, :name, :address, :created_at
11
8
  end
12
9
  end
13
10
 
@@ -53,7 +50,6 @@ describe TestModel do
53
50
  end
54
51
 
55
52
  context "by name" do
56
-
57
53
  it "is valid" do
58
54
  model = FactoryGirl.create(:test_model)
59
55
 
@@ -81,6 +77,15 @@ describe TestModel do
81
77
  end
82
78
 
83
79
  context "By distance" do
80
+ before :each do
81
+ TestModel.class_eval do
82
+ include Paginative::ModelExtension
83
+
84
+ allow_paginative_on :latitude, :longitude
85
+ reverse_geocoded_by :latitude, :longitude
86
+ after_validation :reverse_geocode
87
+ end
88
+ end
84
89
 
85
90
  it "limits the results" do
86
91
  models = FactoryGirl.create_list(:test_model, 30)
@@ -150,4 +155,80 @@ describe TestModel do
150
155
  expect(TestModel.with_field_from(["latitude", "longitude"], [150, 12])).to eq [@third, @fourth]
151
156
  end
152
157
  end
158
+
159
+ context 'restricted fields' do
160
+ before do
161
+ TestModel.paginative_fields = { name: 'test_models.name' }
162
+
163
+ @first = FactoryGirl.create(:test_model, name: 'abc', address: 'abc')
164
+ @second = FactoryGirl.create(:test_model, name: 'abc', address: 'bcd')
165
+ @third = FactoryGirl.create(:test_model, name: 'abc', address: 'cde')
166
+ @fourth = FactoryGirl.create(:test_model, name: 'abc', address: 'def')
167
+ end
168
+
169
+ it 'ignores unpermitted fields with a warning' do
170
+ expect(Rails.logger).to receive(:warn)
171
+
172
+ TestModel.with_field_from('address', 'bcd')
173
+ end
174
+
175
+ it 'prunes the fields to those only permitted' do
176
+ expect(TestModel).to receive(:map_fields).with(['name']) { ['test_models.name'] }
177
+ TestModel.with_field_from(["name", "address"], ["abc", "bcd"])
178
+ end
179
+
180
+ it 'returns the original scope and ordering, still limited' do
181
+ result = TestModel.with_field_from('address', 'bcd', 2)
182
+ expect(result).to eq [@first, @second]
183
+ end
184
+ end
185
+
186
+ describe 'restricting fields' do
187
+ before do
188
+ TestModel.paginative_fields = {}
189
+ end
190
+
191
+ context 'no fields' do
192
+ it 'defaults to no paginative fields' do
193
+ expect(TestModel.paginative_fields).to be_empty
194
+ end
195
+ end
196
+
197
+ context 'self mapped columns' do
198
+ before do
199
+ TestModel.class_eval do
200
+ allow_paginative_on :created_at
201
+ end
202
+ end
203
+
204
+ it 'sets the paginative fields to self mappings' do
205
+ expect(TestModel.paginative_fields).to eq({ created_at: 'test_models.created_at' })
206
+ end
207
+ end
208
+
209
+ context 'join mapped columns' do
210
+ before do
211
+ TestModel.class_eval do
212
+ allow_paginative_on created_at: 'other_models.created_at'
213
+ end
214
+ end
215
+
216
+ it 'sets the paginative fields to the specified mapping' do
217
+ expect(TestModel.paginative_fields).to eq({ created_at: 'other_models.created_at' })
218
+ end
219
+ end
220
+ end
221
+
222
+ describe 'paginating joint fields' do
223
+ before do
224
+ TestModel.paginative_fields = { created_at: 'joint_models.created_at' }
225
+
226
+ @first = FactoryGirl.create(:test_model, name: 'abc', address: 'abc', joint_models: [FactoryGirl.build(:joint_model, created_at: Time.now)])
227
+ @second = FactoryGirl.create(:test_model, name: 'abc', address: 'bcd', joint_models: [FactoryGirl.build(:joint_model, created_at: 5.minutes.ago)])
228
+ end
229
+
230
+ it 'can be paginated on the secondary column (strings)' do
231
+ expect(TestModel.joint.with_field_from('created_at', 3.minutes.ago, 24, 'DESC')).to eq [@second]
232
+ end
233
+ end
153
234
  end
@@ -6,6 +6,7 @@ require 'rspec/rails'
6
6
  require 'rspec/autorun'
7
7
  require 'factory_girl_rails'
8
8
  require 'database_cleaner'
9
+ require 'pry'
9
10
 
10
11
  Rails.backtrace_cleaner.remove_silencers!
11
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paginative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Isaac Norman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-28 00:00:00.000000000 Z
11
+ date: 2015-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -136,6 +136,34 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: 1.2.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: appraisal
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
139
167
  description: After spending a lot of time screwing around with orphaned objects and
140
168
  every other problem that pagination causes, this is the solution
141
169
  email:
@@ -151,11 +179,8 @@ files:
151
179
  - app/assets/stylesheets/paginative/application.css
152
180
  - app/controllers/paginative/application_controller.rb
153
181
  - app/helpers/paginative/application_helper.rb
154
- - app/models/paginative/test_model.rb
155
182
  - app/views/layouts/paginative/application.html.erb
156
183
  - config/routes.rb
157
- - db/migrate/20140415060518_create_paginative_test_models.rb
158
- - db/migrate/20140416020706_add_address_to_test_models.rb
159
184
  - lib/paginative.rb
160
185
  - lib/paginative/engine.rb
161
186
  - lib/paginative/models/model_extension.rb
@@ -169,6 +194,7 @@ files:
169
194
  - spec/dummy/app/assets/stylesheets/application.css
170
195
  - spec/dummy/app/controllers/application_controller.rb
171
196
  - spec/dummy/app/helpers/application_helper.rb
197
+ - spec/dummy/app/models/joint_model.rb
172
198
  - spec/dummy/app/models/test_model.rb
173
199
  - spec/dummy/app/views/layouts/application.html.erb
174
200
  - spec/dummy/bin/bundle
@@ -193,13 +219,14 @@ files:
193
219
  - spec/dummy/config/routes.rb
194
220
  - spec/dummy/config/secrets.yml
195
221
  - spec/dummy/db/migrate/20140416035443_create_test_models.rb
222
+ - spec/dummy/db/migrate/20150922072155_create_joint_models.rb
196
223
  - spec/dummy/db/schema.rb
197
- - spec/dummy/db/test.sqlite3
198
- - spec/dummy/log/test.log
199
224
  - spec/dummy/public/404.html
200
225
  - spec/dummy/public/422.html
201
226
  - spec/dummy/public/500.html
202
227
  - spec/dummy/public/favicon.ico
228
+ - spec/dummy/spec/factories/joint_models.rb
229
+ - spec/factories/paginative_joint_models.rb
203
230
  - spec/factories/paginative_test_models.rb
204
231
  - spec/models/paginative/test_model_spec.rb
205
232
  - spec/spec_helper.rb
@@ -232,6 +259,7 @@ test_files:
232
259
  - spec/dummy/app/assets/stylesheets/application.css
233
260
  - spec/dummy/app/controllers/application_controller.rb
234
261
  - spec/dummy/app/helpers/application_helper.rb
262
+ - spec/dummy/app/models/joint_model.rb
235
263
  - spec/dummy/app/models/test_model.rb
236
264
  - spec/dummy/app/views/layouts/application.html.erb
237
265
  - spec/dummy/bin/bundle
@@ -256,15 +284,16 @@ test_files:
256
284
  - spec/dummy/config/secrets.yml
257
285
  - spec/dummy/config.ru
258
286
  - spec/dummy/db/migrate/20140416035443_create_test_models.rb
287
+ - spec/dummy/db/migrate/20150922072155_create_joint_models.rb
259
288
  - spec/dummy/db/schema.rb
260
- - spec/dummy/db/test.sqlite3
261
- - spec/dummy/log/test.log
262
289
  - spec/dummy/public/404.html
263
290
  - spec/dummy/public/422.html
264
291
  - spec/dummy/public/500.html
265
292
  - spec/dummy/public/favicon.ico
266
293
  - spec/dummy/Rakefile
267
294
  - spec/dummy/README.rdoc
295
+ - spec/dummy/spec/factories/joint_models.rb
296
+ - spec/factories/paginative_joint_models.rb
268
297
  - spec/factories/paginative_test_models.rb
269
298
  - spec/models/paginative/test_model_spec.rb
270
299
  - spec/spec_helper.rb