paginative 0.2.3 → 0.3.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: 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