acts_as_manual_list 0.1.2
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +30 -0
- data/Rakefile +30 -0
- data/lib/acts_as_manual_list.rb +93 -0
- data/lib/acts_as_manual_list/base.rb +16 -0
- data/lib/acts_as_manual_list/list_member.rb +47 -0
- data/lib/acts_as_manual_list/version.rb +5 -0
- data/lib/iknow_list_utils.rb +82 -0
- data/lib/tasks/acts_as_manual_list_tasks.rake +6 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b9339c36ec588ac8f2747aa4051d15433e4f7d2511f63bdf8ddb1a35b260bb1
|
4
|
+
data.tar.gz: 8415269162afd50c21afb3af3c0ffe512796e655467169cc529395b7fe99fa8c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71692f8f11058110b17360d44fe64f323c0b0f073a234e038986d2646ff204390036bc95e094acd2b766e7e4f53d0d5d0538b005e36f1ac96eeda6a77ce5ff77
|
7
|
+
data.tar.gz: 275748d836531bb96f1a51f69ecb6258f3f5060cc44f3c8578e6cc9c664dc21a9690228dace01d92fd36339770739ea8ecae6424ecda0b77eb22c33e11e50653
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 DMM.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
[](https://travis-ci.org/iknow/acts_as_manual_list)
|
2
|
+
|
3
|
+
# ActsAsManualList
|
4
|
+
Short description and motivation.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
How to use my plugin.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'acts_as_manual_list'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
```bash
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
```bash
|
23
|
+
$ gem install acts_as_manual_list
|
24
|
+
```
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
Contribution directions go here.
|
28
|
+
|
29
|
+
## License
|
30
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rdoc/task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'ActsAsManualList'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.md')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
Bundler::GemHelper.install_tasks
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
|
23
|
+
Rake::TestTask.new(:test) do |t|
|
24
|
+
t.libs << 'lib'
|
25
|
+
t.libs << 'test'
|
26
|
+
t.pattern = 'test/**/*_test.rb'
|
27
|
+
t.verbose = false
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: :test
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lazily'
|
4
|
+
require 'iknow_list_utils'
|
5
|
+
|
6
|
+
module ActsAsManualList
|
7
|
+
using IknowListUtils
|
8
|
+
|
9
|
+
POSITION_GETTER = ->(el) { el.send(:position) }
|
10
|
+
POSITION_SETTER = ->(el, v) { el.send(:position=, v) }
|
11
|
+
|
12
|
+
# Takes an list of elements in a desired order. Elements have an inherent
|
13
|
+
# float position, obtainable via `position_getter`, which may be nil if the
|
14
|
+
# element previously was not a list member. For each element whose position
|
15
|
+
# is not in order (i.e. between the positions of its neighbours), calls
|
16
|
+
# `position_setter` to update the position to a new float value.
|
17
|
+
# Returns the count of elements whose positions were changed.
|
18
|
+
def self.update_positions(elements,
|
19
|
+
position_getter: POSITION_GETTER,
|
20
|
+
position_setter: POSITION_SETTER)
|
21
|
+
|
22
|
+
# calculate index for each previously existing position
|
23
|
+
position_indices = elements
|
24
|
+
.lazy
|
25
|
+
.with_index
|
26
|
+
.map { |el, i| [position_getter.(el), i] }
|
27
|
+
.reject { |p, _i| p.nil? }
|
28
|
+
.to_a
|
29
|
+
|
30
|
+
# TODO: we haven't dealt with collisions
|
31
|
+
|
32
|
+
# Calculate stable points in the element array which don't need to have
|
33
|
+
# positions updated:
|
34
|
+
# * before the beginning of the ordered subsequence
|
35
|
+
# * a subsequence of elements which are already in order
|
36
|
+
# * after the end of the ordered subsequence.
|
37
|
+
stable_position_indices = Lazily.concat([[nil, -1]],
|
38
|
+
position_indices.longest_rising_sequence_by(&:first),
|
39
|
+
[[nil, elements.size]])
|
40
|
+
|
41
|
+
# For each possible range of indices that are not stable (i.e. for every
|
42
|
+
# adjacent pair of stable indices), assign new positions distributed
|
43
|
+
# between the stable positions.
|
44
|
+
changed_positions = 0
|
45
|
+
stable_position_indices.each_cons(2) do |(start_pos, start_index), (end_pos, end_index)|
|
46
|
+
range = (start_index + 1)..(end_index - 1)
|
47
|
+
next if range.size.zero?
|
48
|
+
|
49
|
+
changed_positions += range.size
|
50
|
+
new_positions = select_positions(start_pos, end_pos, range.size)
|
51
|
+
|
52
|
+
new_positions.each.with_index(1) do |new_pos, offset|
|
53
|
+
position_setter.(elements[start_index + offset], new_pos)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
changed_positions
|
58
|
+
end
|
59
|
+
|
60
|
+
# Select `count` positions in order between the provided `start_pos` and `end_pos`
|
61
|
+
def self.select_positions(start_pos, end_pos, count)
|
62
|
+
case
|
63
|
+
when start_pos.nil? && end_pos.nil?
|
64
|
+
# all elements are unpositioned, assign sequentially
|
65
|
+
1.upto(count).map(&:to_f)
|
66
|
+
when start_pos.nil?
|
67
|
+
# before first fixed element
|
68
|
+
count.downto(1).map { |i| end_pos - i }
|
69
|
+
when end_pos.nil?
|
70
|
+
# after last fixed element
|
71
|
+
1.upto(count).map { |i| start_pos + i }
|
72
|
+
else
|
73
|
+
delta = (end_pos - start_pos) / (count + 1)
|
74
|
+
1.upto(count).map { |i| start_pos + (delta * i) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Install into ActiveRecord
|
80
|
+
require 'acts_as_manual_list/base'
|
81
|
+
require 'acts_as_manual_list/list_member'
|
82
|
+
|
83
|
+
if defined?(Rails::Railtie)
|
84
|
+
class Railtie < Rails::Railtie
|
85
|
+
initializer 'acts_as_manual_list.insert_into_active_record' do
|
86
|
+
ActiveSupport.on_load :active_record do
|
87
|
+
ActiveRecord::Base.send(:include, ActsAsManualList::Base)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
elsif defined?(ActiveRecord)
|
92
|
+
ActiveRecord::Base.send(:include, ActsAsManualList::Base)
|
93
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
module ActsAsManualList::Base
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def acts_as_manual_list(scope:, attribute: :position)
|
11
|
+
include ActsAsManualList::ListMember
|
12
|
+
self.acts_as_list_scope = scope
|
13
|
+
self.acts_as_list_attribute = attribute
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
module ActsAsManualList::ListMember
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
class << self
|
11
|
+
attr_accessor :acts_as_list_scope, :acts_as_list_attribute
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# TODO: inject something like this into the scope class
|
17
|
+
# def reorder_#{association}(children)
|
18
|
+
# ChildType.set_positions(self, children)
|
19
|
+
# end
|
20
|
+
|
21
|
+
def set_positions(_parent, children)
|
22
|
+
# TODO: validate that the children are all children of the parent.
|
23
|
+
|
24
|
+
# Optimization: if a child record is already `changed?`, then it's going
|
25
|
+
# to be written no matter what. If we don't consider changed records for
|
26
|
+
# selecting stable positions (i.e. ignore their previous `position` field)
|
27
|
+
# we can maximize the chance of avoiding updates to otherwise untouched
|
28
|
+
# rows.
|
29
|
+
get_pos = ->(el) do
|
30
|
+
if el.changed?
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
el.public_send(acts_as_list_attribute)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
set_pos = ->(el, p) { el.public_send(:"#{acts_as_list_attribute}=", p) }
|
38
|
+
|
39
|
+
ActsAsManualList.update_positions(children, position_getter: get_pos, position_setter: set_pos)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# TODO... list motion, append/prepend/etc
|
44
|
+
# append from list
|
45
|
+
# insert at position
|
46
|
+
# remove from list # will need to move from list to list
|
47
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IknowListUtils
|
4
|
+
refine Array do
|
5
|
+
def bsearch_max(min = nil, max = nil)
|
6
|
+
if (idx = self.bsearch_max_index(min, max) { |x| yield(x) })
|
7
|
+
self[idx]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def bsearch_max_index(min = nil, max = nil)
|
12
|
+
# lo and hi are valid array indexes
|
13
|
+
lo = min || 0
|
14
|
+
hi = max || self.count - 1
|
15
|
+
|
16
|
+
# Empty range does not contain any values satisfying predicate
|
17
|
+
return nil if hi < lo
|
18
|
+
|
19
|
+
# Lowest value has to pass predicate
|
20
|
+
return nil unless yield(self[lo])
|
21
|
+
|
22
|
+
# body invariant established: lo is always satisfied
|
23
|
+
|
24
|
+
while lo < hi
|
25
|
+
# when hi = lo + 1, mid = hi; which
|
26
|
+
# - either passes and sets lo' = lo + 1 (= hi) (singleton)
|
27
|
+
# - fails and sets hi' = lo (singleton)
|
28
|
+
# otherwise mid is between both, and the range will be limited appropriately
|
29
|
+
mid = lo + (hi - lo + 1) / 2
|
30
|
+
if yield(self[mid])
|
31
|
+
# midpoint satisfies predicate, reduce bounds, keep satisfying value
|
32
|
+
lo = mid
|
33
|
+
else
|
34
|
+
# midpoint doesn't satisfy predicate, restrict below non-satsifying value
|
35
|
+
hi = mid - 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
lo
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: make this operate on Enumerables instead of directly accessible
|
43
|
+
# collections.
|
44
|
+
def longest_rising_sequence(&compare)
|
45
|
+
# https://en.wikipedia.org/wiki/Longest_increasing_subsequence
|
46
|
+
|
47
|
+
compare ||= ->(x, y) { x <=> y }
|
48
|
+
|
49
|
+
preds = Array.new(self.length)
|
50
|
+
ends = Array.new(self.length + 1)
|
51
|
+
|
52
|
+
max_length = 0
|
53
|
+
|
54
|
+
(0..self.length - 1).each do |i|
|
55
|
+
# bsearch ends for a subsequence to append to, if not found, start
|
56
|
+
# new sequence of length 1
|
57
|
+
existing_sequence_length =
|
58
|
+
ends.bsearch_max_index(1, max_length) do |e|
|
59
|
+
compare.(self[e], self[i]) < 0
|
60
|
+
end
|
61
|
+
|
62
|
+
new_length = (existing_sequence_length || 0) + 1
|
63
|
+
max_length = new_length if max_length < new_length
|
64
|
+
preds[i] = ends[new_length - 1]
|
65
|
+
ends[new_length] = i
|
66
|
+
end
|
67
|
+
|
68
|
+
result = []
|
69
|
+
k = ends[max_length]
|
70
|
+
max_length.downto(1) do |x|
|
71
|
+
result[x - 1] = self[k]
|
72
|
+
k = preds[k]
|
73
|
+
end
|
74
|
+
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
def longest_rising_sequence_by
|
79
|
+
self.longest_rising_sequence { |x, y| yield(x) <=> yield(y) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_manual_list
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- iKnow Team
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: lazily
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.2.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: appraisal
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: ''
|
126
|
+
email:
|
127
|
+
- dev@iknow.jp
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- MIT-LICENSE
|
133
|
+
- README.md
|
134
|
+
- Rakefile
|
135
|
+
- lib/acts_as_manual_list.rb
|
136
|
+
- lib/acts_as_manual_list/base.rb
|
137
|
+
- lib/acts_as_manual_list/list_member.rb
|
138
|
+
- lib/acts_as_manual_list/version.rb
|
139
|
+
- lib/iknow_list_utils.rb
|
140
|
+
- lib/tasks/acts_as_manual_list_tasks.rake
|
141
|
+
homepage: ''
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
metadata: {}
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubygems_version: 3.0.3
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Barebones acts_as_list
|
164
|
+
test_files: []
|