acts_as_graph_diagram 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +178 -0
- data/Rakefile +5 -0
- data/bin/test +7 -0
- data/lib/acts_as_graph_diagram/edge_scopes.rb +23 -0
- data/lib/acts_as_graph_diagram/node/graph_calculator.rb +129 -0
- data/lib/acts_as_graph_diagram/node.rb +120 -0
- data/lib/acts_as_graph_diagram/railtie.rb +11 -0
- data/lib/acts_as_graph_diagram/version.rb +5 -0
- data/lib/acts_as_graph_diagram.rb +12 -0
- data/lib/generators/USAGE +5 -0
- data/lib/generators/acts_as_graph_diagram_generator.rb +30 -0
- data/lib/generators/templates/migration.rb +21 -0
- data/lib/generators/templates/model.rb +24 -0
- data/test/acts_as_graph_diagram_test.rb +44 -0
- data/test/dummy/Rakefile +39 -0
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +1 -0
- data/test/dummy/app/channels/application_cable/channel.rb +6 -0
- data/test/dummy/app/channels/application_cable/connection.rb +6 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/gods_controller.rb +67 -0
- data/test/dummy/app/helpers/application_helper.rb +4 -0
- data/test/dummy/app/helpers/gods_helper.rb +4 -0
- data/test/dummy/app/javascript/application.js +94 -0
- data/test/dummy/app/jobs/application_job.rb +9 -0
- data/test/dummy/app/mailers/application_mailer.rb +6 -0
- data/test/dummy/app/models/application_record.rb +5 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/edge.rb +24 -0
- data/test/dummy/app/models/god.rb +14 -0
- data/test/dummy/app/views/gods/_form.html.erb +22 -0
- data/test/dummy/app/views/gods/_god.html.erb +16 -0
- data/test/dummy/app/views/gods/edit.html.erb +6 -0
- data/test/dummy/app/views/gods/index.html.erb +35 -0
- data/test/dummy/app/views/gods/new.html.erb +5 -0
- data/test/dummy/app/views/gods/show.html.erb +9 -0
- data/test/dummy/app/views/layouts/application.html.erb +15 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/bin/importmap +5 -0
- data/test/dummy/bin/rails +6 -0
- data/test/dummy/bin/rake +6 -0
- data/test/dummy/bin/setup +35 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +7 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/environments/development.rb +71 -0
- data/test/dummy/config/environments/production.rb +89 -0
- data/test/dummy/config/environments/test.rb +62 -0
- data/test/dummy/config/importmap.rb +43 -0
- data/test/dummy/config/initializers/content_security_policy.rb +26 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/test/dummy/config/initializers/inflections.rb +17 -0
- data/test/dummy/config/initializers/permissions_policy.rb +12 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +45 -0
- data/test/dummy/config/routes.rb +11 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +8 -0
- data/test/dummy/db/migrate/20220606102242_create_gods.rb +11 -0
- data/test/dummy/db/migrate/20220612090334_acts_as_graph_diagram_migration.rb +21 -0
- data/test/dummy/db/schema.rb +35 -0
- data/test/dummy/db/seeds.rb +53 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/test/controllers/gods_controller_test.rb +50 -0
- data/test/dummy/test/system/gods_test.rb +43 -0
- data/test/fixtures/edges.yml +111 -0
- data/test/fixtures/gods.yml +81 -0
- data/test/test_helper.rb +18 -0
- metadata +434 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bcef649a07aef82c036b7c6df58bf00513702a32c5960dc3af5d4088267cc618
|
4
|
+
data.tar.gz: fc4a5712c0d0b05f165dc665a324a612cf496ee04eafc69ef587cb1253ee9ecb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1cb85f7ee1c208c5571fa3d447045028917f92d91bf3b70deead1afa01b06a43f5c795b2b9bf324e3b111879ec966fb9b9ff3bced0806b37a9c3b4a65d7a0d11
|
7
|
+
data.tar.gz: c27c720b88ca5ae95f4a4c721f5e625625a49b3ae035e0c140a369cb416aeaf0f72a2411521301203d45a4ecb2f600c5ab7a85697226120dc7774b0e7b7f02aa
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 smapira
|
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,178 @@
|
|
1
|
+
# Acts As Graph Diagram
|
2
|
+
|
3
|
+
Acts As Graph Diagram extends Active Record to add simple function for draw the Force Directed Graph with html.
|
4
|
+
|
5
|
+

|
6
|
+
[](https://badge.fury.io/rb/acts_as_graph_diagram)
|
7
|
+

|
8
|
+
[](https://github.com/rubocop-hq/rubocop)
|
9
|
+
[](https://circleci.com/gh/routeflags/acts_as_graph_diagram)
|
10
|
+
|
11
|
+
## See It Work
|
12
|
+
|
13
|
+

|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Append the line to your model file like below:
|
18
|
+
```ruby
|
19
|
+
class God < ApplicationRecord
|
20
|
+
acts_as_graph_diagram
|
21
|
+
end
|
22
|
+
|
23
|
+
God.find_by(name: 'Rheā').add_destination God.find_by(name: 'Hēra', cost: 1)
|
24
|
+
# => #<Edge:0x000000010b0d4560
|
25
|
+
# id: 1,
|
26
|
+
# comment: "",
|
27
|
+
# cost: 0,
|
28
|
+
# directed: true,
|
29
|
+
# destination_type: "God",
|
30
|
+
# destination_id: 2,
|
31
|
+
# departure_type: "God",
|
32
|
+
# departure_id: 1,
|
33
|
+
# created_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00,
|
34
|
+
# updated_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00>
|
35
|
+
|
36
|
+
God.find_by(name: 'Rheā').connecting_count
|
37
|
+
# => 1
|
38
|
+
|
39
|
+
God.find_by(name: 'Rheā').destinations
|
40
|
+
# => [#<Edge:0x000000010b5642b0
|
41
|
+
# id: 1,
|
42
|
+
# comment: "",
|
43
|
+
# cost: 0,
|
44
|
+
# directed: true,
|
45
|
+
# destination_type: "God",
|
46
|
+
# destination_id: 2,
|
47
|
+
# departure_type: "God",
|
48
|
+
# departure_id: 1,
|
49
|
+
# created_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00,
|
50
|
+
# updated_at: Sun, 12 Jun 2022 11:11:06.995007000 UTC +00:00>]
|
51
|
+
|
52
|
+
God.find_by(name: 'Rheā').aheads.first.destination
|
53
|
+
# => #<God:0x000000010b5efb58 id: 2, name: "Hēra", created_at: Sun, 12 Jun 2022 11:11:06.984341000 UTC +00:00, updated_at: Sun, 12 Jun 2022 11:11:06.984341000 UTC +00:00>
|
54
|
+
```
|
55
|
+
|
56
|
+
### Methods
|
57
|
+
|
58
|
+
* aheads
|
59
|
+
* behinds
|
60
|
+
* add_destination(node, comment: '', cost: 0)
|
61
|
+
* add_departure(node, comment: '', cost: 0)
|
62
|
+
* get_destination(node)
|
63
|
+
* get_departure(node)
|
64
|
+
* remove_destination(node)
|
65
|
+
* remove_departure(node)
|
66
|
+
* connecting?(node)
|
67
|
+
* connecting_count()
|
68
|
+
* add_connection(node, directed: false, comment: '', cost: 0)
|
69
|
+
* sum_cost()
|
70
|
+
* sum_tree_cost()
|
71
|
+
* assemble_tree_nodes()
|
72
|
+
|
73
|
+
### Draws the graph diagram with D3.js
|
74
|
+
|
75
|
+
1. Append the lines to your controller file like below:
|
76
|
+
```ruby
|
77
|
+
class GodsController < ApplicationController
|
78
|
+
def data_network
|
79
|
+
render json: { 'nodes' => God.all.pluck(:id, :name)
|
80
|
+
.map { |x| Hash[id: x[0], name: x[1]] },
|
81
|
+
'links' => Edge.all.pluck(:destination_id, :departure_id)
|
82
|
+
.map { |x| Hash[target: x[0], source: x[1]] } }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
2. And append the line to your routes.rb file like below:
|
88
|
+
```ruby
|
89
|
+
Rails.application.routes.draw do
|
90
|
+
get 'data_network' => 'gods#data_network'
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
3. Then append the line to your javascript file like below:
|
95
|
+
```javascript
|
96
|
+
// v7.4.4
|
97
|
+
d3.json("http://127.0.0.1:3000/data_network").then(function (graph) {});
|
98
|
+
```
|
99
|
+
|
100
|
+
### Calculates the Program Evaluation and Review Technique (PERT)
|
101
|
+
|
102
|
+

|
103
|
+
|
104
|
+
> [PERT Chart. Drawn in Adobe Illustrator - inspired by a chart at netmba.com. Created by Jeremy Kemp. 2005/01/11 From Wikipedia, the free encyclopedia](https://en.wikipedia.org/wiki/Program_evaluation_and_review_technique)
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
Milestone.create(name: 10)
|
108
|
+
Milestone.create(name: 20)
|
109
|
+
Milestone.create(name: 30)
|
110
|
+
Milestone.create(name: 40)
|
111
|
+
Milestone.create(name: 50)
|
112
|
+
|
113
|
+
Milestone.find_by(name: 10).add_destination(Milestone.find_by(name: 20), cost: 3)
|
114
|
+
Milestone.find_by(name: 10).add_destination(Milestone.find_by(name: 30), cost: 4)
|
115
|
+
Milestone.find_by(name: 30).add_destination(Milestone.find_by(name: 40), cost: 1)
|
116
|
+
Milestone.find_by(name: 40).add_destination(Milestone.find_by(name: 50), cost: 3)
|
117
|
+
Milestone.find_by(name: 30).add_destination(Milestone.find_by(name: 50), cost: 2)
|
118
|
+
Milestone.find_by(name: 20).add_destination(Milestone.find_by(name: 50), cost: 3)
|
119
|
+
|
120
|
+
Milestone.find_by(name: 10).sum_tree_cost
|
121
|
+
|
122
|
+
# => 16
|
123
|
+
```
|
124
|
+
|
125
|
+
## Installation
|
126
|
+
Add this line to your application's Gemfile:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
gem "acts_as_graph_diagram"
|
130
|
+
```
|
131
|
+
|
132
|
+
And then execute:
|
133
|
+
```bash
|
134
|
+
$ bundle
|
135
|
+
$ bin/rails generate acts_as_graph_diagram
|
136
|
+
$ bin/rails db:migrate
|
137
|
+
```
|
138
|
+
|
139
|
+
Or install it yourself as:
|
140
|
+
```bash
|
141
|
+
$ gem install acts_as_graph_diagram
|
142
|
+
```
|
143
|
+
|
144
|
+
## Development
|
145
|
+
### Rails console
|
146
|
+
```bash
|
147
|
+
test/dummy/bin/rails console
|
148
|
+
```
|
149
|
+
|
150
|
+
### Test
|
151
|
+
```bash
|
152
|
+
bin/test
|
153
|
+
```
|
154
|
+
|
155
|
+
## Contributing
|
156
|
+
Bug reports and pull requests are welcome on Github at https://github.com/routeflags/acts_as_graph_diagram. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
|
157
|
+
|
158
|
+
## License
|
159
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
160
|
+
|
161
|
+
## Changelog
|
162
|
+
available [here](https://github.com/routeflags/acts_as_graph_diagram/main/CHANGELOG.md).
|
163
|
+
|
164
|
+
## Code of Conduct
|
165
|
+
Everyone interacting in the ActsAsTreeDiagram project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/routeflags/acts_as_graph_diagram/main/CODE_OF_CONDUCT.md).
|
166
|
+
|
167
|
+
## You may enjoy owning other libraries and my company.
|
168
|
+
|
169
|
+
* [acts_as_graph_diagram: ActsAsTreeDiagram extends ActsAsTree to add simple function for draw tree diagram with html.](https://github.com/routeflags/acts_as_graph_diagram)
|
170
|
+
* [timeline_rails_helper: The TimelineRailsHelper provides a timeline_molecules_tag helper to draw a vertical time line usable with vanilla CSS.](https://github.com/routeflags/timeline_rails_helper)
|
171
|
+
* [株式会社旗指物](https://blog.routeflags.com/)
|
172
|
+
|
173
|
+
## Аcknowledgments
|
174
|
+
|
175
|
+
- [activerecord - Model an undirected graph in Rails? - Stack Overflow](https://stackoverflow.com/questions/7976301/model-an-undirected-graph-in-rails)
|
176
|
+
- [tcocca/acts_as_follower: A Gem to add Follow functionality for models](https://github.com/tcocca/acts_as_follower)
|
177
|
+
- [Force layout | D3 in Depth](https://www.d3indepth.com/force-layout/)
|
178
|
+
- [Rubyを使って「なぜ関数プログラミングは重要か」を読み解く(改定)─ 前編 ─ 但し後編の予定なし](https://melborne.github.io/2013/01/21/why-fp-with-ruby/)
|
data/Rakefile
ADDED
data/bin/test
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsGraphDiagram # :nodoc:
|
4
|
+
##
|
5
|
+
# This module represents a act of edge.
|
6
|
+
module EdgeScopes
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
# returns Edge records where destination is the record passed in.
|
11
|
+
# @param [Node] node
|
12
|
+
scope :select_destinations, ->(node) { where(destination_id: node.id, destination_type: node.class.name) }
|
13
|
+
|
14
|
+
# returns Edge records where departure is the record passed in.
|
15
|
+
# @param [Node] node
|
16
|
+
scope :select_departures, ->(node) { where(departure_id: node.id, departure_type: node.class.name) }
|
17
|
+
|
18
|
+
# returns Edge records where departure or destination are the record passed in.
|
19
|
+
# @param [Node] node
|
20
|
+
scope :select_connections, ->(node) { select_destinations(node).or(select_departures(node)) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsGraphDiagram # :nodoc:
|
4
|
+
##
|
5
|
+
# This module represents a array of node.
|
6
|
+
class Nodes < Array
|
7
|
+
# @param [Proc] functional
|
8
|
+
# @param [Proc] meta
|
9
|
+
# @param [Array] values
|
10
|
+
# @param [Symbol] operator
|
11
|
+
# @return Proc
|
12
|
+
def read_tree(functional, meta, values, operator)
|
13
|
+
return values if !defined?(empty?) || empty?
|
14
|
+
|
15
|
+
meta[first.destination.read_tree(functional, meta, values, operator),
|
16
|
+
ActsAsGraphDiagram::Nodes.new(tail)
|
17
|
+
.read_tree(functional, meta, values, operator)]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Proc] functional
|
21
|
+
# @param [Nodes] values
|
22
|
+
# @return Proc
|
23
|
+
def linear(functional, values)
|
24
|
+
return self if empty? || !first.attributes.include?(:destination)
|
25
|
+
|
26
|
+
functional[first.destination,
|
27
|
+
ActsAsGraphDiagram::Nodes.new(tail)
|
28
|
+
.linear(functional, values)]
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return Array
|
32
|
+
def tail
|
33
|
+
drop 1
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return Proc
|
37
|
+
def confluence
|
38
|
+
# @param [Proc] functional
|
39
|
+
# @param [Nodes] values
|
40
|
+
lambda do |x, list = self|
|
41
|
+
ActsAsGraphDiagram::Nodes.new([x] + list)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return Proc
|
46
|
+
def append
|
47
|
+
# @param [Proc] functional
|
48
|
+
# @param [Nodes] values
|
49
|
+
lambda do |nodes = self, list|
|
50
|
+
nodes.linear confluence,
|
51
|
+
ActsAsGraphDiagram::Nodes.new(list)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module Node # :nodoc:
|
57
|
+
##
|
58
|
+
# This module represents a calculation of graph.
|
59
|
+
module GraphCalculator
|
60
|
+
extend ActiveSupport::Concern
|
61
|
+
|
62
|
+
included do
|
63
|
+
# @param [Proc] functional
|
64
|
+
# @param [Proc] meta
|
65
|
+
# @param [Any] value
|
66
|
+
# @param [Symbol] operator
|
67
|
+
# @return Proc
|
68
|
+
def read_tree(functional, meta, value, operator = :self)
|
69
|
+
argument = if operator == :self
|
70
|
+
self
|
71
|
+
else
|
72
|
+
public_send(operator)
|
73
|
+
end
|
74
|
+
functional[argument,
|
75
|
+
ActsAsGraphDiagram::Nodes
|
76
|
+
.new(aheads.where.not(destination_id: id).to_a)
|
77
|
+
.read_tree(functional, meta, value, operator)]
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return Integer
|
81
|
+
def sum_tree_cost
|
82
|
+
read_tree addition, addition, 0, :sum_cost
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return Proc
|
86
|
+
def addition
|
87
|
+
# @param [Any] x
|
88
|
+
# @param [Any] y
|
89
|
+
->(x, y) { x + y }
|
90
|
+
end
|
91
|
+
|
92
|
+
def sum_cost
|
93
|
+
aheads.sum(:cost)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param [Proc] functional
|
97
|
+
# @param [Proc] meta
|
98
|
+
# @return Proc
|
99
|
+
def linear(functional, meta)
|
100
|
+
raise NotImplementedError
|
101
|
+
->(x, y) { functional[meta[x], y] }
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return Proc
|
105
|
+
def append
|
106
|
+
# @param [Nodes] nodes
|
107
|
+
# @param [Array] values
|
108
|
+
lambda do |nodes = self, values|
|
109
|
+
nodes.linear confluence, ActsAsGraphDiagram::Nodes.new(values)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return Proc
|
114
|
+
def confluence
|
115
|
+
# @param [Node] node
|
116
|
+
# @param [Nodes] nodes
|
117
|
+
lambda do |node, nodes = self|
|
118
|
+
ActsAsGraphDiagram::Nodes.new([node] + nodes)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [Node]
|
123
|
+
def assemble_tree_nodes
|
124
|
+
read_tree confluence, append, []
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acts_as_graph_diagram/node/graph_calculator'
|
4
|
+
|
5
|
+
module ActsAsGraphDiagram # :nodoc:
|
6
|
+
module Node
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods # :nodoc:
|
12
|
+
def acts_as_node
|
13
|
+
has_many :behinds, as: :destination,
|
14
|
+
class_name: 'Edge',
|
15
|
+
dependent: :destroy
|
16
|
+
has_many :aheads, as: :departure,
|
17
|
+
class_name: 'Edge',
|
18
|
+
dependent: :destroy
|
19
|
+
include ActsAsGraphDiagram::Node::InstanceMethods
|
20
|
+
include ActsAsGraphDiagram::Node::GraphCalculator
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods # :nodoc:
|
25
|
+
# rubocop:disable Style/HashSyntax
|
26
|
+
|
27
|
+
# Creates a new destination record for this instance to connect the passed object.
|
28
|
+
# @param [Node] node
|
29
|
+
# @param [String] comment
|
30
|
+
# @param [Integer] cost
|
31
|
+
# @return [Edge]
|
32
|
+
def add_destination(node, comment: '', cost: 0)
|
33
|
+
aheads.select_destinations(node)
|
34
|
+
.where(comment: comment, cost: cost)
|
35
|
+
.first_or_create!
|
36
|
+
end
|
37
|
+
|
38
|
+
# Creates a new departure record for this instance to connect the passed object.
|
39
|
+
# @param [Node] node
|
40
|
+
# @param [String] comment
|
41
|
+
# @param [Integer] cost
|
42
|
+
# @return [Edge]
|
43
|
+
def add_departure(node, comment: '', cost: 0)
|
44
|
+
behinds.select_departures(node)
|
45
|
+
.where(comment: comment, cost: cost)
|
46
|
+
.first_or_create!
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates a new undirected connection record for this instance to connect the passed object.
|
50
|
+
# @param [Node] node
|
51
|
+
# @param [Boolean] directed
|
52
|
+
# @param [String] comment
|
53
|
+
# @param [Integer] cost
|
54
|
+
# @return [Edge]
|
55
|
+
def add_connection(node, directed: false, comment: '', cost: 0)
|
56
|
+
Edge.where(destination: node,
|
57
|
+
directed: directed,
|
58
|
+
departure: self,
|
59
|
+
comment: comment,
|
60
|
+
cost: cost).first_or_create!
|
61
|
+
end
|
62
|
+
# rubocop:enable Style/HashSyntax
|
63
|
+
|
64
|
+
# Returns a destination node record for the current instance.
|
65
|
+
# @param [Node] node
|
66
|
+
# @return [Edge]
|
67
|
+
def get_destination(node)
|
68
|
+
aheads.select_destinations(node).first
|
69
|
+
end
|
70
|
+
|
71
|
+
# Deletes the destination record if it exists.
|
72
|
+
# @param [Node] node
|
73
|
+
# @return [Edge|nil]
|
74
|
+
def remove_destination(node)
|
75
|
+
get_destination(node).try(:destroy)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns a departure node record for the current instance.
|
79
|
+
# @param [Node] node
|
80
|
+
# @return [Edge]
|
81
|
+
def get_departure(node)
|
82
|
+
behinds.select_departures(node).first
|
83
|
+
end
|
84
|
+
|
85
|
+
# Deletes the destination record if it exists.
|
86
|
+
# @param [Node] node
|
87
|
+
# @return [Edge|nil]
|
88
|
+
def remove_departure(node)
|
89
|
+
get_departure(node).try(:destroy)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a undirected node record for the current instance.
|
93
|
+
# @param [Node] node
|
94
|
+
# @return [Edge]
|
95
|
+
def get_connection(node)
|
96
|
+
Edge.select_connections(node).first
|
97
|
+
end
|
98
|
+
|
99
|
+
# Deletes the undirected record if it exists.
|
100
|
+
# @param [Node] node
|
101
|
+
# @return [Edge|nil]
|
102
|
+
def remove_connection(node)
|
103
|
+
get_connection(node).try(:destroy)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns true if this instance is connecting the object passed as an argument.
|
107
|
+
# @param [Node] node
|
108
|
+
# @return [Boolean]
|
109
|
+
def connecting?(node)
|
110
|
+
node.connecting_count.positive?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the number of objects this instance is following.
|
114
|
+
# @return [Integer]
|
115
|
+
def connecting_count
|
116
|
+
Edge.select_connections(self).count
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsGraphDiagram # :nodoc:
|
4
|
+
class Railtie < ::Rails::Railtie # :nodoc:
|
5
|
+
initializer 'acts_as_graph_diagram.active_record' do |_app|
|
6
|
+
ActiveSupport.on_load :active_record do
|
7
|
+
include ActsAsGraphDiagram::Node
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acts_as_graph_diagram/version'
|
4
|
+
require 'acts_as_graph_diagram/railtie'
|
5
|
+
|
6
|
+
# Specify this extension if you want to model a graph
|
7
|
+
# network structure by providing any edge association.
|
8
|
+
module ActsAsGraphDiagram
|
9
|
+
autoload :Node, 'acts_as_graph_diagram/node'
|
10
|
+
autoload :EdgeScopes, 'acts_as_graph_diagram/edge_scopes.rb'
|
11
|
+
require 'acts_as_graph_diagram/railtie' if defined?(Rails) && Rails::VERSION::MAJOR >= 3
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/migration'
|
5
|
+
|
6
|
+
class ActsAsGraphDiagramGenerator < Rails::Generators::Base # :nodoc:
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Implement the required interface for Rails::Generators::Migration.
|
14
|
+
# taken from https://github.com/rails/rails/blob/master/activerecord/lib/rails/generators/active_record.rb
|
15
|
+
def self.next_migration_number(dirname)
|
16
|
+
if ActiveRecord::Base.timestamped_migrations
|
17
|
+
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
18
|
+
else
|
19
|
+
format('%.3d', (current_migration_number(dirname) + 1))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_migration_file
|
24
|
+
migration_template 'migration.rb', 'db/migrate/acts_as_graph_diagram_migration.rb'
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_model
|
28
|
+
template 'model.rb', File.join('app/models', 'edge.rb')
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActsAsGraphDiagramMigration < ActiveRecord::Migration[4.2] # :nodoc:
|
4
|
+
def self.up
|
5
|
+
create_table :edges, force: true do |t|
|
6
|
+
t.string :comment, default: ''
|
7
|
+
t.integer :cost, default: 0
|
8
|
+
t.boolean :directed, default: true
|
9
|
+
t.references :destination, polymorphic: true, null: true
|
10
|
+
t.references :departure, polymorphic: true, null: true
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :edges, %w[departure_id departure_type], name: 'fk_edges_departure'
|
15
|
+
add_index :edges, %w[destination_id destination_type], name: 'fk_edges_destination'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :edges
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: edges
|
6
|
+
#
|
7
|
+
# id :integer not null, primary key
|
8
|
+
# comment :string default("")
|
9
|
+
# cost :integer default(0)
|
10
|
+
# directed :boolean default(TRUE)
|
11
|
+
# destination_type :string
|
12
|
+
# destination_id :integer
|
13
|
+
# departure_type :string
|
14
|
+
# departure_id :integer
|
15
|
+
# created_at :datetime
|
16
|
+
# updated_at :datetime
|
17
|
+
#
|
18
|
+
class Edge < ActiveRecord::Base
|
19
|
+
extend ActsAsGraphDiagram::Node
|
20
|
+
include ActsAsGraphDiagram::EdgeScopes
|
21
|
+
|
22
|
+
belongs_to :destination, polymorphic: true, optional: true
|
23
|
+
belongs_to :departure, polymorphic: true, optional: true
|
24
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class ActsAsGraphDiagramTest < ActiveSupport::TestCase
|
6
|
+
test 'it has a version number' do
|
7
|
+
assert ActsAsGraphDiagram::VERSION
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'be defined' do
|
11
|
+
assert God.first.respond_to?(:aheads)
|
12
|
+
assert God.first.respond_to?(:behinds)
|
13
|
+
assert God.first.respond_to?(:add_destination)
|
14
|
+
assert God.first.respond_to?(:add_departure)
|
15
|
+
assert God.first.respond_to?(:get_destination)
|
16
|
+
assert God.first.respond_to?(:get_departure)
|
17
|
+
assert God.first.respond_to?(:remove_destination)
|
18
|
+
assert God.first.respond_to?(:remove_departure)
|
19
|
+
assert God.first.respond_to?(:connecting?)
|
20
|
+
assert God.first.respond_to?(:connecting_count)
|
21
|
+
assert God.first.respond_to?(:add_connection)
|
22
|
+
assert God.first.respond_to?(:sum_cost)
|
23
|
+
assert God.first.respond_to?(:sum_tree_cost)
|
24
|
+
assert God.first.respond_to?(:assemble_tree_nodes)
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'calculate sum_cost' do
|
28
|
+
God.find(3).add_destination(God.find(5), cost: 4)
|
29
|
+
assert_equal God.find(3).sum_cost, 4
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'calculate sum_tree_cost' do
|
33
|
+
God.find(4).add_destination(God.find(6), cost: 4)
|
34
|
+
God.find(6).add_destination(God.find(7), cost: 3)
|
35
|
+
assert_equal God.find(4).sum_tree_cost, 7
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'call assemble_tree_nodes' do
|
39
|
+
God.find(4).add_destination(God.find(6), cost: 4)
|
40
|
+
God.find(6).add_destination(God.find(7), cost: 3)
|
41
|
+
God.find(6).add_destination(God.find(7), cost: 3)
|
42
|
+
assert_equal God.find(4).assemble_tree_nodes.size, 3
|
43
|
+
end
|
44
|
+
end
|