acts_as_graph_diagram 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Ruby](https://img.shields.io/badge/Ruby-CC342D?style=for-the-badge&logo=ruby&logoColor=white)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/acts_as_graph_diagram.svg)](https://badge.fury.io/rb/acts_as_graph_diagram)
|
7
|
+
![](https://ruby-gem-downloads-badge.herokuapp.com/acts_as_graph_diagram)
|
8
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop-hq/rubocop)
|
9
|
+
[![CircleCI](https://circleci.com/gh/routeflags/acts_as_graph_diagram.svg?style=svg)](https://circleci.com/gh/routeflags/acts_as_graph_diagram)
|
10
|
+
|
11
|
+
## See It Work
|
12
|
+
|
13
|
+
![acts_as_graph_diagram](https://user-images.githubusercontent.com/25024587/173231019-ede998c4-333a-48dd-b2da-96f04e1fce86.gif)
|
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
|
+
![Pert_chart_colored](https://user-images.githubusercontent.com/25024587/174105277-213a955a-b783-43ae-be98-1174d9256273.gif)
|
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
|