active_record_florder 0.0.1

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.
@@ -0,0 +1,307 @@
1
+ {
2
+ "RSpec": {
3
+ "coverage": {
4
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder.rb": [
5
+ 1,
6
+ 1,
7
+ 1,
8
+ 1,
9
+ 1,
10
+ 1,
11
+ null,
12
+ null,
13
+ 1,
14
+ 1,
15
+ 1,
16
+ 0,
17
+ null,
18
+ null,
19
+ 1,
20
+ 1862,
21
+ null,
22
+ null,
23
+ 1,
24
+ null,
25
+ null,
26
+ null,
27
+ 1,
28
+ 1628,
29
+ null,
30
+ null,
31
+ 1,
32
+ 330,
33
+ null,
34
+ null,
35
+ null,
36
+ null,
37
+ 1
38
+ ],
39
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder/error.rb": [
40
+ 1,
41
+ 1,
42
+ null
43
+ ],
44
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder/configurable.rb": [
45
+ 1,
46
+ 1,
47
+ 1,
48
+ 1,
49
+ 47,
50
+ null,
51
+ null,
52
+ 1,
53
+ 2050,
54
+ null,
55
+ null,
56
+ 1,
57
+ 2042,
58
+ null,
59
+ null,
60
+ 1,
61
+ 1866,
62
+ null,
63
+ null,
64
+ 1,
65
+ 352,
66
+ null,
67
+ null,
68
+ null,
69
+ 1,
70
+ 0,
71
+ null,
72
+ null,
73
+ 1,
74
+ 1909,
75
+ null,
76
+ null,
77
+ 1,
78
+ 1021,
79
+ null,
80
+ null,
81
+ 1,
82
+ 1857,
83
+ null,
84
+ null,
85
+ 1,
86
+ 265,
87
+ null,
88
+ null,
89
+ null
90
+ ],
91
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder/base.rb": [
92
+ 1,
93
+ null,
94
+ 1,
95
+ null,
96
+ 1,
97
+ 1,
98
+ null,
99
+ 1,
100
+ 3,
101
+ 3,
102
+ null,
103
+ 246,
104
+ null,
105
+ null,
106
+ 3,
107
+ 934,
108
+ 87,
109
+ null,
110
+ null,
111
+ 3,
112
+ 47,
113
+ 47,
114
+ null,
115
+ null,
116
+ 3,
117
+ 8,
118
+ 86,
119
+ null,
120
+ null,
121
+ null,
122
+ null,
123
+ 1,
124
+ 417,
125
+ null,
126
+ 417,
127
+ 415,
128
+ 411,
129
+ null,
130
+ 411,
131
+ null,
132
+ 411,
133
+ 243,
134
+ null,
135
+ 168,
136
+ null,
137
+ null,
138
+ 411,
139
+ null,
140
+ null,
141
+ 1,
142
+ null,
143
+ 1,
144
+ 50,
145
+ null,
146
+ 50,
147
+ 32,
148
+ null,
149
+ 18,
150
+ null,
151
+ null,
152
+ null,
153
+ 1,
154
+ null,
155
+ 1,
156
+ 32,
157
+ 32,
158
+ 32,
159
+ null,
160
+ 32,
161
+ 4,
162
+ null,
163
+ null,
164
+ 32,
165
+ 0,
166
+ null,
167
+ null,
168
+ 32,
169
+ 32,
170
+ null,
171
+ null,
172
+ 32,
173
+ 0,
174
+ null,
175
+ null,
176
+ 32,
177
+ null,
178
+ null,
179
+ 1,
180
+ 4,
181
+ null,
182
+ null,
183
+ 1,
184
+ 639,
185
+ 89,
186
+ null,
187
+ null,
188
+ 176,
189
+ null,
190
+ 176,
191
+ null,
192
+ 176,
193
+ null,
194
+ null,
195
+ 1,
196
+ 176,
197
+ null,
198
+ 176,
199
+ null,
200
+ 156,
201
+ null,
202
+ 20,
203
+ null,
204
+ null,
205
+ null,
206
+ null,
207
+ 1,
208
+ 411,
209
+ 411,
210
+ null,
211
+ 411,
212
+ null,
213
+ null,
214
+ null,
215
+ null,
216
+ 411,
217
+ null,
218
+ null,
219
+ 1,
220
+ 41,
221
+ 46,
222
+ 46,
223
+ 46,
224
+ null,
225
+ null,
226
+ null,
227
+ 1,
228
+ 46,
229
+ null,
230
+ null,
231
+ 1,
232
+ 226,
233
+ null,
234
+ null,
235
+ null,
236
+ null,
237
+ 226,
238
+ null,
239
+ null,
240
+ 1,
241
+ 226,
242
+ null,
243
+ null,
244
+ 9,
245
+ null,
246
+ null,
247
+ null,
248
+ 41,
249
+ null,
250
+ null,
251
+ 156,
252
+ null,
253
+ null,
254
+ 20,
255
+ null,
256
+ null,
257
+ 0,
258
+ 0,
259
+ null,
260
+ null,
261
+ null,
262
+ null,
263
+ 1,
264
+ 858,
265
+ null,
266
+ 858,
267
+ 781,
268
+ null,
269
+ 77,
270
+ null,
271
+ null,
272
+ null,
273
+ null,
274
+ null,
275
+ 1,
276
+ 934,
277
+ 87,
278
+ null,
279
+ null,
280
+ null
281
+ ],
282
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder/models.rb": [
283
+ 1,
284
+ 1,
285
+ 1,
286
+ 3,
287
+ 3,
288
+ 3,
289
+ null,
290
+ 3,
291
+ 4,
292
+ null,
293
+ null,
294
+ 3,
295
+ null,
296
+ null,
297
+ null
298
+ ],
299
+ "/Users/marek/Developer/activerecord-florder/lib/active_record_florder/version.rb": [
300
+ 1,
301
+ 1,
302
+ null
303
+ ]
304
+ },
305
+ "timestamp": 1452102822
306
+ }
307
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/active-model-florder_test.sqlite3
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/active-model-florder_test.sqlite3
@@ -0,0 +1,6 @@
1
+ class CreateOwners < ActiveRecord::Migration
2
+ def change
3
+ create_table(:owners) do |t|
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ class CreateMovables < ActiveRecord::Migration
2
+ def change
3
+ create_table(:movables) do |t|
4
+ t.references :owner
5
+ t.float :position, default: 0
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ class AddPosition2ToMovables < ActiveRecord::Migration
2
+ def change
3
+ add_column :movables, :position_2, :float, default: 0
4
+ end
5
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended that you check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(version: 20160103190528) do
15
+
16
+ create_table "movables", force: true do |t|
17
+ t.integer "owner_id"
18
+ t.float "position", default: 0.0
19
+ t.float "position_2", default: 0.0
20
+ end
21
+
22
+ create_table "owners", force: true do |t|
23
+ end
24
+
25
+ end
@@ -0,0 +1,236 @@
1
+ require 'active_support'
2
+
3
+ module ActiveRecordFlorder
4
+
5
+ # Base model implements all required logic
6
+ # It's depend on ActiveRecordFlorder::Configurable attributes
7
+ module Base
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+
12
+ # Include configuration methods
13
+ extend ActiveRecordFlorder::Configurable::ClassMethods
14
+ include ActiveRecordFlorder::Configurable
15
+
16
+ # Set initial position new records
17
+ before_create -> { push(:highest) }
18
+
19
+ # Scope all positions solving
20
+ # @api public
21
+ scope :position_scope, lambda { |value|
22
+ return all unless position_scope_attr.present? && value.present?
23
+ where(position_scope_attr.to_sym => value)
24
+ }
25
+
26
+ # Public Class method for getting models in right order
27
+ # @api public
28
+ scope :ordered, lambda {
29
+ direction = florder_direction.to_sym == :desc ? 'DESC' : 'ASC'
30
+ order("#{position_attr_name} #{direction}")
31
+ }
32
+
33
+ # Generates optimal positions, do not affect order
34
+ # @api public
35
+ def self.reinit_positions
36
+ all.order("#{position_attr_name} ASC").each_with_index do |model, index|
37
+ model.update_attribute(position_attr_name.to_sym, (index + 1) * next_position_step)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Move model to position
43
+ # @param position {Float}
44
+ # @returns self {ModelInstance}
45
+ # @api public
46
+ def move(position)
47
+ position.to_f
48
+
49
+ fail ActiveRecordFlorder::Error, 'Position param is required' unless position
50
+ fail ActiveRecordFlorder::Error, 'Position should be > 0' unless (normalized_position = normalize_position(position)) > 0
51
+ normalized_position = normalize_position(position)
52
+
53
+ ensure_position_solving(position, normalized_position)
54
+
55
+ if new_record?
56
+ self[position_attr_name.to_sym] = normalized_position
57
+ else
58
+ update_attribute(position_attr_name.to_sym, normalized_position)
59
+ end
60
+
61
+ self
62
+ end
63
+
64
+ protected
65
+
66
+ # Increase/decrease models position but do not affect order
67
+ # @param direction [:increase, :decrease]
68
+ def slide(direction, ensured_position)
69
+ sibling = get_sibling(direction)
70
+
71
+ if sibling
72
+ slide_to_sibling(direction, sibling, ensured_position)
73
+ else
74
+ push(direction == :increase ? :highest : :lowest)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ # Increase/decrease models position for cases where it should respect siblings
81
+ # @param direction [:increase, :desrease]
82
+ # @param sibling {Object}
83
+ # @param ensured_position {Float}
84
+ def slide_to_sibling(direction, sibling, ensured_position)
85
+ sibling_position = sibling.try(position_attr_name.to_sym)
86
+ position = send(position_attr_name.to_sym)
87
+ new_position = (position + sibling_position) / 2
88
+
89
+ if (new_position - ensured_position).abs < min_position_delta
90
+ new_position = get_slide_edge_position(direction, ensured_position)
91
+ end
92
+
93
+ unless (normalize_position(new_position) > 0)
94
+ return self.class.reinit_positions
95
+ end
96
+
97
+ min = new_position - min_position_delta
98
+ max = new_position + min_position_delta
99
+
100
+ self.class.position_scope(scope_value)
101
+ .where("#{position_attr_name} > ? AND #{position_attr_name} < ?", max, min).each do |conflict|
102
+ c.slide(direction)
103
+ end
104
+
105
+ move(new_position)
106
+ end
107
+
108
+ # Helper for getting value to SELECTconflict in one direction
109
+ # @param direction [:increase, :decrease]
110
+ # @param ensured_position {Float}
111
+ # @returns {Float}
112
+ def get_slide_edge_position(direction, ensured_position)
113
+ direction == :increase ? ensured_position + min_position_delta : ensured_position - min_position_delta
114
+ end
115
+
116
+ # Send model to end or begening
117
+ # @param place [:highest, :lowest]
118
+ def push(place)
119
+ unless self.class.position_scope(scope_value).any? { |m| m.position.present? }
120
+ return move(next_position_step)
121
+ end
122
+
123
+ sibling_position = get_sibling(place).try(position_attr_name.to_sym)
124
+
125
+ position = calc_push_position(place, sibling_position)
126
+
127
+ move(position)
128
+ end
129
+
130
+ # Calculate Edge (highest or lowest) positions based on current position of edge record
131
+ # @param place [:highest, :lowest]
132
+ # @param sibling_position {Float}
133
+ def calc_push_position(place, sibling_position)
134
+ return unless sibling_position
135
+
136
+ case place
137
+ when :highest
138
+ sibling_position + next_position_step
139
+ when :lowest
140
+ sibling_position / 2.0
141
+ end
142
+ end
143
+
144
+ # Find all models with conflicting position and solve conflicts
145
+ # @param position {Float}
146
+ # @param normalized_position {Float}
147
+ def ensure_position_solving(position, normalized_position)
148
+ min = normalized_position - min_position_delta
149
+ max = normalized_position + min_position_delta
150
+
151
+ conflicts = self.class.position_scope(scope_value)
152
+ .where("#{position_attr_name} > ? AND #{position_attr_name} < ?", min, max)
153
+ .where.not(id: id)
154
+ .order(:position)
155
+
156
+ position_conflict_solver(conflicts, position, normalized_position) if conflicts.present?
157
+ end
158
+
159
+ # Resolve all given conflicts
160
+ # @param conflicts {Array}
161
+ # @param position {Float}
162
+ # @param normalized_position {Float}
163
+ def position_conflict_solver(conflicts, position, normalized_position)
164
+ conflicts.each do |conflict|
165
+ conflict_position = conflict.send(position_attr_name.to_sym)
166
+ direction = get_conflict_direction(position, conflict_position)
167
+ conflict.slide(direction, normalized_position)
168
+ end
169
+ end
170
+
171
+ # Helper condition for deciding conflict solving direction
172
+ # @param position {Float}
173
+ # @param conflict_position {Float}
174
+ # @returns Symbol [:increase, :decrease]
175
+ def get_conflict_direction(position, conflict_position)
176
+ conflict_position > position ? :increase : :decrease
177
+ end
178
+
179
+ # Sibblings getter for given vector
180
+ # @place [:increase, :decrease, :highest, :lowest]
181
+ # @returns Object
182
+ def get_sibling(vector)
183
+ conditions = get_siblings_conditions(vector)
184
+
185
+ self.class.position_scope(scope_value)
186
+ .where(conditions.first)
187
+ .order(conditions.last)
188
+ .limit(1).first
189
+ end
190
+
191
+ # get paramets for sibling SELECT
192
+ # @param vector [:increase, :decrease, :highest, :lowest]
193
+ # @returns Array
194
+ def get_siblings_conditions(vector)
195
+ case vector
196
+ when :increase
197
+ [["#{position_attr_name} > ?",
198
+ send(position_attr_name.to_sym)],
199
+ "#{position_attr_name} DESC"]
200
+ when :decrease
201
+ [["#{position_attr_name} < ?",
202
+ send(position_attr_name.to_sym)],
203
+ "#{position_attr_name} ASC"]
204
+ when :highest
205
+ [nil,
206
+ "#{position_attr_name} DESC"]
207
+ when :lowest
208
+ [nil,
209
+ "#{position_attr_name} ASC"]
210
+ else
211
+ error_message = "Place param '#{place}' is not one of: increase, decrease, highest, lowest."
212
+ fail ActiveRecordFlorder::Error, error_message
213
+ end
214
+ end
215
+
216
+ # returns normalized position (roundend value)
217
+ # @param position {Float}
218
+ # @returns Float
219
+ def normalize_position(position)
220
+ splitted_value = min_position_delta.to_s.split('.')
221
+
222
+ if splitted_value.size.to_i > 1
223
+ position.round(splitted_value.last.size)
224
+ else
225
+ (position / 10**(min_position_delta.to_s.size - 1)).round
226
+ end
227
+ end
228
+
229
+ # Scope helper
230
+ # returns value of scope attr
231
+ def scope_value
232
+ return unless position_scope_attr
233
+ send(position_scope_attr)
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveRecordFlorder
2
+
3
+ # Handle configuration loading and storing
4
+ # defines getters for configurable attributes
5
+ module Configurable
6
+
7
+ # Uses instance class variables
8
+ module ClassMethods
9
+ def florder_direction
10
+ @florder_config[:type]
11
+ end
12
+
13
+ def position_attr_name
14
+ @florder_config[:attribute] || ActiveRecordFlorder.get_attribute
15
+ end
16
+
17
+ def position_scope_attr
18
+ @florder_config[:scope] || ActiveRecordFlorder.get_scope
19
+ end
20
+
21
+ def min_position_delta
22
+ @florder_config[:min_delta] || ActiveRecordFlorder.get_min_delta
23
+ end
24
+
25
+ def next_position_step
26
+ @florder_config[:step] || ActiveRecordFlorder.get_step
27
+ end
28
+ end
29
+
30
+ # All instance methods are just proxy
31
+ # To Class Methods
32
+ def florder_direction
33
+ self.class.florder_direction
34
+ end
35
+
36
+ def position_attr_name
37
+ self.class.position_attr_name
38
+ end
39
+
40
+ def position_scope_attr
41
+ self.class.position_scope_attr
42
+ end
43
+
44
+ def min_position_delta
45
+ self.class.min_position_delta
46
+ end
47
+
48
+ def next_position_step
49
+ self.class.next_position_step
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveRecordFlorder
2
+ # Error class
3
+ class Error < StandardError; end
4
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecordFlorder
2
+
3
+ # Mixin for extending ActiveRecord::Base
4
+ # These methods are available in Models
5
+ module Models
6
+
7
+ # Initialize ActiveRecordFlorder with given options
8
+ # @param type [:asc, :desc], required!
9
+ # @param opts {Hash}, optional
10
+ def florder(type, opts = {})
11
+
12
+ # require type
13
+ fail ActiveRecordFlorder::Error, 'Define florder type :asc/:desc' unless [:asc, :desc].include?(type)
14
+
15
+ # setup configuration var
16
+ @florder_config = {}
17
+ @florder_config[:type] = type.to_sym
18
+
19
+ # add opts to config
20
+ opts.each do |key, value|
21
+ @florder_config[key.to_sym] = value
22
+ end
23
+
24
+ # include florder mixin
25
+ include ActiveRecordFlorder::Base
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRecordFlorder
2
+ VERSION = "0.0.1".freeze
3
+ end