active_record_florder 0.0.1

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