in_order 0.1.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +759 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/in_order_manifest.js +2 -0
  6. data/app/assets/javascripts/in_order/application.js +15 -0
  7. data/app/assets/stylesheets/in_order/application.css +15 -0
  8. data/app/controllers/in_order/application_controller.rb +5 -0
  9. data/app/controllers/in_order/concerns/response_helpers.rb +33 -0
  10. data/app/controllers/in_order/elements_controller.rb +47 -0
  11. data/app/controllers/in_order/lists_controller.rb +86 -0
  12. data/app/helpers/in_order/application_helper.rb +4 -0
  13. data/app/jobs/in_order/application_job.rb +4 -0
  14. data/app/mailers/in_order/application_mailer.rb +6 -0
  15. data/app/models/in_order/add.rb +51 -0
  16. data/app/models/in_order/application_record.rb +5 -0
  17. data/app/models/in_order/aux/create_element.rb +17 -0
  18. data/app/models/in_order/aux/element_iterator.rb +39 -0
  19. data/app/models/in_order/aux/get_element.rb +17 -0
  20. data/app/models/in_order/aux/get_keys.rb +27 -0
  21. data/app/models/in_order/aux/keys.rb +58 -0
  22. data/app/models/in_order/aux/poly_find.rb +24 -0
  23. data/app/models/in_order/aux/poly_key.rb +117 -0
  24. data/app/models/in_order/aux/position_base.rb +25 -0
  25. data/app/models/in_order/aux/repair.rb +71 -0
  26. data/app/models/in_order/aux/sort_elements.rb +32 -0
  27. data/app/models/in_order/aux/var_keys.rb +20 -0
  28. data/app/models/in_order/create.rb +62 -0
  29. data/app/models/in_order/element.rb +119 -0
  30. data/app/models/in_order/fetch.rb +33 -0
  31. data/app/models/in_order/insert.rb +42 -0
  32. data/app/models/in_order/move.rb +15 -0
  33. data/app/models/in_order/purge.rb +71 -0
  34. data/app/models/in_order/queue.rb +65 -0
  35. data/app/models/in_order/remove.rb +29 -0
  36. data/app/models/in_order/stack.rb +12 -0
  37. data/app/models/in_order/trim.rb +74 -0
  38. data/app/models/in_order/update.rb +83 -0
  39. data/app/models/in_order.rb +9 -0
  40. data/app/views/in_order/lists/_list.html.erb +16 -0
  41. data/app/views/in_order/lists/index.html.erb +9 -0
  42. data/app/views/layouts/in_order/application.html.erb +16 -0
  43. data/config/routes.rb +10 -0
  44. data/db/migrate/20190814101433_create_in_order_elements.rb +15 -0
  45. data/lib/in_order/engine.rb +5 -0
  46. data/lib/in_order/version.rb +3 -0
  47. data/lib/in_order.rb +5 -0
  48. data/lib/tasks/in_order_tasks.rake +4 -0
  49. metadata +119 -0
@@ -0,0 +1,71 @@
1
+
2
+ module InOrder
3
+ module Aux
4
+ class Repair
5
+ attr_accessor :dry_run, :owners, :elements
6
+ alias dry_run? dry_run
7
+
8
+ def initialize(_dry_run=false, dry_run: _dry_run)
9
+ self.dry_run = dry_run
10
+
11
+ self.owners = []
12
+ self.elements = []
13
+ end
14
+
15
+ def call
16
+ InOrder::Element.transaction do
17
+ unreferenced :owner do |type_id|
18
+ owners << type_id * '-'
19
+
20
+ InOrder::Element.by_owner_keys(*type_id).delete_all unless dry_run?
21
+ end
22
+
23
+ unlinked :subject do |element|
24
+ elements << element
25
+
26
+ InOrder::Remove.new(element).call unless dry_run?
27
+ end
28
+ end
29
+
30
+ return owners, elements
31
+ end
32
+
33
+ class << self
34
+ def ic
35
+ owners, elements = new(true).call
36
+
37
+ puts show 'Owners', owners
38
+ puts show 'Elements', elements
39
+ end
40
+
41
+ def show(name, list)
42
+ out = name << ' ' << list.size.to_s << "\n"
43
+
44
+ out << list.map(&:to_param) * " " << "\n"
45
+ end
46
+ end
47
+
48
+ def unlinked(name)
49
+ unreferenced(name).map do |type_id|
50
+ InOrder::Element.by_subject_keys(*type_id).first.tap do |element|
51
+ yield element if block_given?
52
+ end
53
+ end
54
+ end
55
+
56
+ def unreferenced(name)
57
+ linked(name).reject do |(type, id)|
58
+ type.constantize.where(id: id).exists?
59
+ end
60
+ end
61
+
62
+ def linked(name)
63
+ fields = [ :"#{name}_type", :"#{name}_id" ]
64
+
65
+ InOrder::Element.select(*fields).distinct.pluck(*fields)
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,32 @@
1
+
2
+ module InOrder
3
+ module Aux
4
+ module SortElements
5
+ module_function \
6
+ def sort_elements(elements)
7
+ index = elements.size
8
+
9
+ sorted = Array.new(index)
10
+
11
+ element_id = nil
12
+
13
+ while index > 0
14
+ index -= 1
15
+
16
+ element = elements.find do |element|
17
+ element.element_id == element_id
18
+ end
19
+
20
+ if element
21
+ sorted[index] = element
22
+
23
+ element_id = element.id
24
+ end
25
+ end
26
+
27
+ sorted
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,20 @@
1
+
2
+ module InOrder
3
+ module Aux
4
+ module VarKeys
5
+ include GetKeys
6
+
7
+ def self.included(base)
8
+ base.instance_eval { attr_accessor :keys }
9
+ end
10
+
11
+ # +keys+ InOrder::Keys or an Array, used as constructor args to former
12
+ def initialize(*keys)
13
+ super()
14
+
15
+ self.keys = get_keys(keys)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,62 @@
1
+
2
+ module InOrder
3
+ # Creates a list of elements for a given Owner and/or Scope,
4
+ # which make up the key.
5
+ # This list links a number of arbitrary *ActiveRecords* together,
6
+ # retaining the given order (sequence).
7
+ # Owner and Record are any ActiveRecord model (linked polymorphically).
8
+ # The Scope is a string with an application-dependent value.
9
+ # At least one of Owner and Scope must be given.
10
+ # If a list of elememts with the same Owner/Scope combo already exists,
11
+ # then the new elements will be appended to them,
12
+ # unless the 'append' parameter is given as *false*,
13
+ # in which case, they'll be prepended.
14
+ class Create
15
+ include Aux::VarKeys
16
+ include Aux::CreateElement
17
+
18
+ # +models+ are an Araay of ActiveRecord models to be linked
19
+ def call(models, append: true)
20
+ InOrder::Element.transaction do
21
+ if append
22
+ previous_last_element = InOrder::Element.last_element(keys)
23
+ else
24
+ previous_first_element = InOrder::Element.first_element(keys)
25
+ end
26
+
27
+ add_elements(models.dup).tap do |created_elements|
28
+ if created_elements.any?
29
+ if previous_first_element
30
+ InOrder::Element.link_elements created_elements.last,
31
+ previous_first_element
32
+ elsif previous_last_element
33
+ InOrder::Element.link_elements previous_last_element,
34
+ created_elements.first
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ alias elements call
41
+
42
+ def subjects(*args)
43
+ call(*args).map &:subject
44
+ end
45
+ alias records subjects
46
+
47
+ protected
48
+
49
+ def add_elements(models, created=[], element_id=nil)
50
+ if record = models&.pop
51
+ element = create_element(record, keys, element_id)
52
+
53
+ add_elements models, created, element.id
54
+
55
+ created << element
56
+ else
57
+ created
58
+ end
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,119 @@
1
+
2
+ module InOrder
3
+ # An element of a persisted linked-list
4
+ class Element < ApplicationRecord
5
+ extend Aux::GetKeys
6
+
7
+ self.table_name_prefix = 'in_order_'
8
+
9
+ belongs_to :owner, optional: true, polymorphic: true
10
+
11
+ belongs_to :subject, polymorphic: true
12
+
13
+ belongs_to :element, optional: true
14
+
15
+ scope :by_scope, -> (scope=nil) { where scope: scope }
16
+
17
+ scope :by_owner, -> owner { where owner: owner }
18
+
19
+ scope :by_owner_keys, -> (type, id) { where owner_type: type, owner_id: id }
20
+
21
+ scope :by_subject, -> subject { where subject: subject }
22
+
23
+ scope :by_subject_keys, -> (type, id) { where subject_type: type, subject_id: id }
24
+
25
+ scope :by_keys, -> keys { by_scope(keys.scope).by_owner(keys.owner) }
26
+
27
+ scope :by_element, -> element { where element_id: element.to_param }
28
+
29
+ scope :include_all, -> { includes :subject, :owner }
30
+
31
+ scope :fetch, -> keys { by_keys(keys).include_all }
32
+
33
+ scope :null_element, -> { where element_id: nil }
34
+
35
+ validate :validate_has_key_fields_valued
36
+
37
+ alias call subject
38
+
39
+ def to_s
40
+ call.to_s
41
+ end
42
+
43
+ def as_json(*)
44
+ super except: %i(created_at updated_at),
45
+ #include: :subject,
46
+ methods: :value
47
+ end
48
+ def value
49
+ subject.as_json
50
+ end
51
+
52
+ def to_keys
53
+ Aux::Keys.new(owner, scope)
54
+ end
55
+
56
+ def iterator
57
+ Aux::ElementIterator.new(to_keys)
58
+ end
59
+
60
+ class << self
61
+ def find_with_keys(*keys)
62
+ keys = get_keys(keys)
63
+
64
+ by_owner_keys(*keys.owner_key.to_a).by_scope(keys.scope)
65
+ end
66
+ alias find_with_key find_with_keys
67
+
68
+ def fetch_with_keys(*keys)
69
+ find_with_keys(keys).include_all
70
+ end
71
+ alias fetch_with_key fetch_with_keys
72
+
73
+ def delete_elements(*keys)
74
+ transaction do
75
+ by_keys(get_keys keys).map {|element| element.destroy }
76
+ end
77
+ end
78
+
79
+ def delete_list(*keys)
80
+ by_keys(get_keys keys).delete_all
81
+ end
82
+
83
+ def first_element(*keys)
84
+ keyed = by_keys(get_keys keys)
85
+
86
+ if keyed.exists?
87
+ keyed.where.not(id: keyed.pluck(:element_id)).first
88
+ end
89
+ end
90
+
91
+ def last_element(*keys)
92
+ by_keys(get_keys keys).null_element.first
93
+ end
94
+
95
+ def for_subject(*keys)
96
+ poly_key = Aux::PolyKey.new(yield)
97
+
98
+ by_keys(get_keys keys).by_subject_keys(*poly_key.to_a)
99
+ end
100
+
101
+ def has_subject?(*keys, &block)
102
+ for_subject(keys, &block).exists?
103
+ end
104
+
105
+ def link_elements(end_of_former, beginning_of_latter)
106
+ end_of_former.update_attribute :element, beginning_of_latter
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def validate_has_key_fields_valued
113
+ unless scope.present? or owner.present?
114
+ errors[:base] << "Cannot leave both scope and owner blank"
115
+ end
116
+ end
117
+ end
118
+ end
119
+
@@ -0,0 +1,33 @@
1
+
2
+ module InOrder
3
+ # Returns a list of elements in order, given a key of Owner and/or Scope.
4
+ class Fetch
5
+ include Aux::VarKeys
6
+
7
+ include Aux::SortElements
8
+
9
+ def call
10
+ elements.map &:subject
11
+ end
12
+
13
+ def elements
14
+ sort_elements fetch
15
+ end
16
+
17
+ def fetch
18
+ InOrder::Element.fetch_with_key(keys).to_a
19
+ end
20
+
21
+ def repair
22
+ InOrder::Element.transaction do
23
+ fetch.each do |element|
24
+ if element.subject.nil?
25
+ Remove.new(element).call
26
+ end
27
+ end
28
+ end
29
+ self
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,42 @@
1
+
2
+ module InOrder
3
+ # Puts a new element anywhere in a pre-exising list.
4
+ class Insert < Aux::PositionBase
5
+ extend Aux::CreateElement
6
+ extend Aux::GetElement
7
+
8
+ def call
9
+ InOrder::Element.transaction do
10
+ if marker
11
+ if after?
12
+ target.update element_id: marker.element_id
13
+
14
+ marker.update element_id: target.id
15
+ else
16
+ if previous = InOrder::Element.find_by(element_id: marker.id)
17
+ previous.update element_id: target.id
18
+ end
19
+
20
+ target.update element_id: marker.id
21
+ end
22
+ end
23
+ target
24
+ end
25
+ end
26
+
27
+ def self.call(record, marker, adjacency, keys: nil)
28
+ keys = InOrder::Aux::Keys.new(*keys) if Array === keys
29
+
30
+ unless ActiveRecord::Base === record
31
+ record = InOrder::Aux::PolyFind.new(record).call
32
+ end
33
+
34
+ marker = get_element(marker)
35
+
36
+ element = create_element(record, keys || marker.to_keys)
37
+
38
+ new(element, marker, adjacency).call
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,15 @@
1
+
2
+ module InOrder
3
+ # Repositions an existing list element.
4
+ # Can be used in drag'n'drop lists.
5
+ class Move < Aux::PositionBase
6
+ def call
7
+ InOrder::Element.transaction do
8
+ Remove.new(target, destroy: false).call
9
+
10
+ Insert.new(target, marker, adjacency).call
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,71 @@
1
+
2
+ module InOrder
3
+ # Removes unwanted subjects from a list.
4
+ # Identified (i.e. keyed) by an Owner and/or Scope.
5
+ class Purge
6
+ include Aux::VarKeys
7
+
8
+ # +keep_last+ determines whether the first or last occurrence remains
9
+ def call(keep_last: false)
10
+ elements = get_elements
11
+
12
+ elements.reverse! if keep_last
13
+
14
+ traverse elements do |element, found_subjects, subject|
15
+ Remove.new(element).call if found_subjects.include?(subject)
16
+ end
17
+ end
18
+ alias uniq call
19
+
20
+ def remove(*subjects, elements: get_elements)
21
+ did_remove = false
22
+
23
+ subjects.flatten! if Array === subjects.first
24
+
25
+ traverse elements do |element|
26
+ if subjects.include?(element.subject)
27
+ Remove.new(element).call
28
+
29
+ did_remove = true
30
+ end
31
+ end
32
+
33
+ did_remove
34
+ end
35
+
36
+ def exists?(subject)
37
+ InOrder::Element.has_subject?(keys) { subject }
38
+ end
39
+
40
+ def self.delete_by_subject(subject)
41
+ InOrder::Element.transaction do
42
+ InOrder::Element.by_subject(subject).each do |element|
43
+ Remove.new(element).call
44
+ end
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def traverse(elements)
51
+ found_subjects = []
52
+
53
+ InOrder::Element.transaction do
54
+ elements.each do |element|
55
+ subject = element.subject
56
+
57
+ yield element, found_subjects, subject
58
+
59
+ found_subjects << subject
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def get_elements
67
+ Fetch.new(keys).elements
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,65 @@
1
+
2
+ # A first in, last out queue.
3
+ module InOrder
4
+ class Queue
5
+ include Aux::VarKeys
6
+
7
+ def join(record, max=nil)
8
+ adder.prepend(record).tap { trim(max) if max }
9
+ end
10
+ alias add join
11
+
12
+ def call
13
+ remove_element
14
+ end
15
+ alias leave call
16
+ alias take call
17
+
18
+ def peek
19
+ last_element&.subject
20
+ end
21
+
22
+ def trim(max, take_last=is_queue?)
23
+ if size > max
24
+ if take_last
25
+ remove_element
26
+ else
27
+ remove_element { first_element }
28
+ end
29
+
30
+ trim max, take_last
31
+ end
32
+ end
33
+
34
+ def size
35
+ InOrder::Element.by_keys(keys).count
36
+ end
37
+
38
+ private
39
+
40
+ def last_element
41
+ InOrder::Element.last_element(keys)
42
+ end
43
+
44
+ def first_element
45
+ InOrder::Element.first_element(keys)
46
+ end
47
+
48
+ def remove_element
49
+ if element = (block_given? ? yield : last_element)
50
+ Remove.new(element).call
51
+
52
+ element.subject
53
+ end
54
+ end
55
+
56
+ def adder
57
+ Add.new(keys)
58
+ end
59
+
60
+ def is_queue?
61
+ InOrder::Queue == self.class
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,29 @@
1
+
2
+ module InOrder
3
+ # Takes an element out of a list
4
+ class Remove
5
+ include Aux::GetElement
6
+
7
+ attr_accessor :element_id, :destroy
8
+ alias destroy? destroy
9
+
10
+ def initialize(element_id, destroy: true)
11
+ self.element_id = element_id
12
+
13
+ self.destroy = destroy
14
+ end
15
+
16
+ def call
17
+ InOrder::Element.transaction do
18
+ element = get_element element_id
19
+
20
+ if previous = InOrder::Element.find_by(element_id: element.id)
21
+ previous.update element_id: element.element_id
22
+ end
23
+
24
+ element.tap {|element| element.destroy if destroy? }
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,12 @@
1
+
2
+ # A first in, first out queue.
3
+ module InOrder
4
+ class Stack < InOrder::Queue
5
+ def push(record, max=nil)
6
+ adder.append(record).tap { trim(max) if max }
7
+ end
8
+
9
+ alias pop call
10
+ end
11
+ end
12
+
@@ -0,0 +1,74 @@
1
+
2
+ module InOrder
3
+ # Ensures a list does not exceed a specified length
4
+ class Trim
5
+ include Aux::SortElements
6
+
7
+ attr_accessor :destroy, :max, :take_from
8
+ alias destroy? destroy
9
+
10
+ def initialize(_max=nil, destroy: true, take_from: :bottom, max: _max)
11
+ self.destroy = destroy
12
+
13
+ self.max = max&.to_i
14
+
15
+ self.take_from = take_from
16
+ end
17
+
18
+ def call(elements, max_size=max)
19
+ return unless max_size
20
+
21
+ elements = elements.dup
22
+
23
+ InOrder::Element.transaction do
24
+ while elements.size > max_size.to_i
25
+ if take_from == :bottom
26
+ delete elements.pop
27
+
28
+ unlink elements.last
29
+ else
30
+ delete elements.shift
31
+ end
32
+ end
33
+ end
34
+ elements
35
+ end
36
+
37
+ class << self
38
+ mattr_accessor :maximum, default: 10
39
+
40
+ def call(*keys)
41
+ elements = InOrder::Fetch.new(*keys).elements
42
+
43
+ args = block_given? ? yield : [ maximum, destroy: false ]
44
+
45
+ InOrder::Trim.new(*args).call(elements).map(&:subject)
46
+ end
47
+
48
+ def set_max(max)
49
+ self.maximum = max
50
+
51
+ self
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def delete(element)
58
+ element.destroy if destroy? and is_an_element!(element)
59
+ end
60
+
61
+ def unlink(element)
62
+ element.update(element_id: nil) if destroy?
63
+ end
64
+
65
+ def is_an_element!(element)
66
+ if InOrder::Element === element
67
+ true
68
+ else
69
+ raise "Cannot trim: #{element.class} is not an instance of InOrder::Element"
70
+ end
71
+ end
72
+ end
73
+ end
74
+