positioning 0.2.5 → 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
  SHA256:
3
- metadata.gz: '04080c2e27b8d0545c86d69fb4a7794530971ca19c1e9db969181ff01bab2f96'
4
- data.tar.gz: 3e81bbd7406e0221378d78e4404629c70eb84d82849d96c6a3506982cbfdf756
3
+ metadata.gz: 2b3dd60b08d2b1488b5fd6c88681f83cd37b46dd2eb0f27557ca4fb131928c9b
4
+ data.tar.gz: 442d3030455cadbcaa2e4fe0f97bc4631d9081194aff31c67242ec5675bc5f8e
5
5
  SHA512:
6
- metadata.gz: ef7cd59860371c435080925d38d994ac8b6a538de4c3a2687f602959792d7d9d1ba91c3df8c91c74ecf284f65ac432d7fc88b2d7ea3710c22c5f27641398f58e
7
- data.tar.gz: 108ded7f5f7756f05dbc19b863bc723ed4e32e41737b6eecded5d0a6e0ab8a19e65a05bb330ce3625f5a61736ed0c4f816a3183efde8b7d17d0bcfbd5518ddda
6
+ metadata.gz: 2c199c7f7d79ca57833593e3bfbe2e47161875943aabef8247c9cabb7b35e5c22297b6b69f298fc2c3df8ee0e9c5f1d99dc6c41e8e90b979cddd205c2d4b91b6
7
+ data.tar.gz: f6e070723c2178c5b7e4907e43ae8fcf2a9f50611b37179fc618ffa220205382f01f8aaa907a0a0cea1bd2aa0e14baf623401678bebc5e7b5ccbc80f2f3f31eb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-08-21
4
+
5
+ - POSSIBLY BREAKING: Clear all position columns on a duplicate created with `dup`.
6
+
7
+ ## [0.2.6] - 2024-08-21
8
+
9
+ - Implement list healing so that existing lists can be fixed up when implementing `positioned` or if the list somehow gets corrupted.
10
+ - Tidy up Advisory Lock code.
11
+
3
12
  ## [0.2.5] - 2024-08-10
4
13
 
5
14
  - Implemented composite primary key support. Thanks @jackozi for the original PR and the nudge to get this done!
data/README.md CHANGED
@@ -72,6 +72,36 @@ belongs_to :list
72
72
  positioned on: :list, advisory_lock: false
73
73
  ```
74
74
 
75
+ ### Initialising a List
76
+
77
+ If you are adding `positioning` to a model with existing database records, or you're migrating from another gem like `acts_as_list` or `ranked-model` and have an existing position column, you will need to do some work to ensure you have well formed position values for your records. `positioning` has a helper method per `positioned` declaration that allows you to 'heal' the position column, ensuring that positions are positive integers starting at 1 with no gaps.
78
+
79
+ For example, in the usual case:
80
+
81
+ ```
82
+ belongs_to :list
83
+ positioned on: :list
84
+ ```
85
+
86
+ you'll have a method called `heal_position_column!`. You can call this method and it will cycle through every existing scope combination in your database (every list with items in this case) and reset those items' position based on their current position order by default. You can pass in a custom order if you don't trust (or don't have) an existing order column. The custom order is passed through to the Active Record `reorder` method, so you can provide anything that that method accepts:
87
+
88
+ ```
89
+ Item.heal_position_column! name: :desc
90
+ ```
91
+
92
+ You may need to introduce your database constraints after healing your position column:
93
+
94
+ * We recommend a `null: false` constraint on the position column but if your existing column has `NULL` values, you'll need to fix those first. The heal method will heal `NULL` positions but depending on your database engine `NULL` positioned items might be placed at the start of the returned records or at the end (if positioning on the position column). Some databases allow this behaviour to be customised.
95
+ * We also recommend a unique index on the scope columns and the position column. If you have repeated position integers per scope you'll need to use the heal method to fix these first before applying the unique index in a separate migration step.
96
+
97
+ The heal method name is named after the column used to store position values. By default this is `position` but if you override it then the method name will change:
98
+
99
+ ```
100
+ positioned on: :category, column: :category_position
101
+ ```
102
+
103
+ will have a class method named `heal_category_position_column!`.
104
+
75
105
  ### Manipulating Positioning
76
106
 
77
107
  The tools for manipulating the position of records in your list have been kept intentionally terse. Priority has also been given to minimal pollution of the model namespace. Only two class methods are defined on all models (`positioning_columns` and `positioned`), and two instance methods are defined on models that call `positioned`:
@@ -155,6 +185,10 @@ other_item.id # => 11
155
185
  item.update position: {after: 11}
156
186
  ```
157
187
 
188
+ ##### Duplicating (`dup`)
189
+
190
+ When you call `dup` on an instance in the list, all position columns on the duplicate will be set to `nil` so that when this duplicate is saved it will be added either to the end of the current scopes (if unchanged) or to the end of any new scopes. Of course you can then override the position of the duplicate before you save it if necessary.
191
+
158
192
  ##### Relative Positioning in Forms
159
193
 
160
194
  It can be tricky to provide the hash forms of relative positioning using Rails form helpers, but it is possible. We've declared a special `Struct` for you to use for this purpose.
@@ -3,23 +3,24 @@ require "openssl"
3
3
 
4
4
  module Positioning
5
5
  class AdvisoryLock
6
- Adapter = Struct.new(:initialise, :aquire, :release, keyword_init: true)
6
+ Adapter = Struct.new(:initialise, :acquire, :release, keyword_init: true)
7
7
 
8
8
  attr_reader :base_class
9
9
 
10
- def initialize(base_class, column)
10
+ def initialize(base_class, column, enabled)
11
11
  @base_class = base_class
12
12
  @column = column.to_s
13
+ @enabled = enabled
13
14
 
14
15
  @adapters = {
15
16
  "mysql2" => Adapter.new(
16
17
  initialise: -> {},
17
- aquire: -> { connection.execute "SELECT GET_LOCK(#{connection.quote(lock_name)}, -1)" },
18
+ acquire: -> { connection.execute "SELECT GET_LOCK(#{connection.quote(lock_name)}, -1)" },
18
19
  release: -> { connection.execute "SELECT RELEASE_LOCK(#{connection.quote(lock_name)})" }
19
20
  ),
20
21
  "postgresql" => Adapter.new(
21
22
  initialise: -> {},
22
- aquire: -> { connection.execute "SELECT pg_advisory_lock(#{lock_name.hex & 0x7FFFFFFFFFFFFFFF})" },
23
+ acquire: -> { connection.execute "SELECT pg_advisory_lock(#{lock_name.hex & 0x7FFFFFFFFFFFFFFF})" },
23
24
  release: -> { connection.execute "SELECT pg_advisory_unlock(#{lock_name.hex & 0x7FFFFFFFFFFFFFFF})" }
24
25
  ),
25
26
  "sqlite3" => Adapter.new(
@@ -28,7 +29,7 @@ module Positioning
28
29
  filename = "#{Dir.pwd}/tmp/#{lock_name}.lock"
29
30
  @file ||= File.open filename, File::RDWR | File::CREAT, 0o644
30
31
  },
31
- aquire: -> {
32
+ acquire: -> {
32
33
  @file.flock File::LOCK_EX
33
34
  },
34
35
  release: -> {
@@ -37,24 +38,23 @@ module Positioning
37
38
  )
38
39
  }
39
40
 
40
- @adapters.default = Adapter.new(initialise: -> {}, aquire: -> {}, release: -> {})
41
+ @adapters.default = Adapter.new(initialise: -> {}, acquire: -> {}, release: -> {})
41
42
 
42
- adapter.initialise.call
43
+ adapter.initialise.call if @enabled
43
44
  end
44
45
 
45
- def aquire(record)
46
- adapter.aquire.call
47
- end
46
+ def acquire
47
+ adapter.acquire.call if @enabled
48
48
 
49
- def release(record)
50
- adapter.release.call
49
+ if block_given?
50
+ yield
51
+ adapter.release.call if @enabled
52
+ end
51
53
  end
52
54
 
53
- alias_method :before_create, :aquire
54
- alias_method :before_update, :aquire
55
- alias_method :before_destroy, :aquire
56
- alias_method :after_commit, :release
57
- alias_method :after_rollback, :release
55
+ def release
56
+ adapter.release.call if @enabled
57
+ end
58
58
 
59
59
  private
60
60
 
@@ -0,0 +1,31 @@
1
+ module Positioning
2
+ class Healer
3
+ def initialize(model, column, order)
4
+ @model = model
5
+ @column = column.to_sym
6
+ @order = order
7
+ end
8
+
9
+ def heal
10
+ if positioning_columns.present?
11
+ @model.select(*positioning_columns).distinct.each do |scope_record|
12
+ sequence @model.where(scope_record.slice(*positioning_columns))
13
+ end
14
+ else
15
+ sequence @model
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def positioning_columns
22
+ @model.positioning_columns[@column]
23
+ end
24
+
25
+ def sequence(scope)
26
+ scope.reorder(@order).each.with_index(1) do |record, index|
27
+ record.update_columns @column => index
28
+ end
29
+ end
30
+ end
31
+ end
@@ -6,11 +6,11 @@ module Positioning
6
6
  end
7
7
 
8
8
  def prior
9
- positioning_scope.where("#{@column}": position - 1).first
9
+ positioning_scope.where(@column => position - 1).first
10
10
  end
11
11
 
12
12
  def subsequent
13
- positioning_scope.where("#{@column}": position + 1).first
13
+ positioning_scope.where(@column => position + 1).first
14
14
  end
15
15
 
16
16
  def create_position
@@ -84,17 +84,17 @@ module Positioning
84
84
 
85
85
  def move_out_of_the_way
86
86
  position_was # Memoize the original position before changing it
87
- record_scope.update_all "#{@column}": 0
87
+ record_scope.update_all @column => 0
88
88
  end
89
89
 
90
90
  def expand(scope, range)
91
- scope.where("#{@column}": range).update_all "#{quoted_column} = #{quoted_column} * -1"
92
- scope.where("#{@column}": ..-1).update_all "#{quoted_column} = #{quoted_column} * -1 + 1"
91
+ scope.where(@column => range).update_all "#{quoted_column} = #{quoted_column} * -1"
92
+ scope.where(@column => ..-1).update_all "#{quoted_column} = #{quoted_column} * -1 + 1"
93
93
  end
94
94
 
95
95
  def contract(scope, range)
96
- scope.where("#{@column}": range).update_all "#{quoted_column} = #{quoted_column} * -1"
97
- scope.where("#{@column}": ..-1).update_all "#{quoted_column} = #{quoted_column} * -1 - 1"
96
+ scope.where(@column => range).update_all "#{quoted_column} = #{quoted_column} * -1"
97
+ scope.where(@column => ..-1).update_all "#{quoted_column} = #{quoted_column} * -1 - 1"
98
98
  end
99
99
 
100
100
  def solidify_position
@@ -1,3 +1,3 @@
1
1
  module Positioning
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/positioning.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative "positioning/version"
2
2
  require_relative "positioning/advisory_lock"
3
3
  require_relative "positioning/mechanisms"
4
+ require_relative "positioning/healer"
4
5
 
5
6
  require "active_support/concern"
6
7
  require "active_support/lazy_load_hooks"
@@ -43,25 +44,35 @@ module Positioning
43
44
  super(position)
44
45
  end
45
46
 
46
- if advisory_lock
47
- advisory_lock_callback = AdvisoryLock.new(base_class, column)
47
+ advisory_locker = AdvisoryLock.new(base_class, column, advisory_lock)
48
48
 
49
- before_create advisory_lock_callback
50
- before_update advisory_lock_callback
51
- before_destroy advisory_lock_callback
52
- end
49
+ before_create { advisory_locker.acquire }
50
+ before_update { advisory_locker.acquire }
51
+ before_destroy { advisory_locker.acquire }
53
52
 
54
53
  before_create { Mechanisms.new(self, column).create_position }
55
54
  before_update { Mechanisms.new(self, column).update_position }
56
55
  before_destroy { Mechanisms.new(self, column).destroy_position }
57
56
 
58
- if advisory_lock
59
- after_commit advisory_lock_callback
60
- after_rollback advisory_lock_callback
57
+ after_commit { advisory_locker.release }
58
+ after_rollback { advisory_locker.release }
59
+
60
+ define_singleton_method(:"heal_#{column}_column!") do |order = column|
61
+ advisory_locker.acquire do
62
+ Healer.new(self, column, order).heal
63
+ end
61
64
  end
62
65
  end
63
66
  end
64
67
  end
68
+
69
+ def initialize_dup(other)
70
+ super
71
+
72
+ self.class.positioning_columns.keys.each do |positioning_column|
73
+ send :"#{positioning_column}=", nil
74
+ end
75
+ end
65
76
  end
66
77
  end
67
78
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: positioning
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brendon Muir
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-09 00:00:00.000000000 Z
11
+ date: 2024-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -123,6 +123,7 @@ files:
123
123
  - Rakefile
124
124
  - lib/positioning.rb
125
125
  - lib/positioning/advisory_lock.rb
126
+ - lib/positioning/healer.rb
126
127
  - lib/positioning/mechanisms.rb
127
128
  - lib/positioning/version.rb
128
129
  - positioning.gemspec