zuul 0.1.1 → 0.2.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 (80) hide show
  1. data/lib/generators/zuul/orm_helpers.rb +21 -0
  2. data/lib/generators/zuul/permission_generator.rb +57 -0
  3. data/lib/generators/zuul/permission_role_generator.rb +40 -0
  4. data/lib/generators/zuul/permission_subject_generator.rb +40 -0
  5. data/lib/generators/zuul/role_generator.rb +58 -0
  6. data/lib/generators/zuul/role_subject_generator.rb +40 -0
  7. data/lib/generators/zuul/subject_generator.rb +39 -0
  8. data/lib/generators/zuul/templates/permission.rb +18 -0
  9. data/lib/generators/zuul/templates/permission_existing.rb +25 -0
  10. data/lib/generators/zuul/templates/permission_role.rb +17 -0
  11. data/lib/generators/zuul/templates/permission_role_existing.rb +24 -0
  12. data/lib/generators/zuul/templates/permission_subject.rb +17 -0
  13. data/lib/generators/zuul/templates/permission_subject_existing.rb +24 -0
  14. data/lib/generators/zuul/templates/role.rb +20 -0
  15. data/lib/generators/zuul/templates/role_existing.rb +27 -0
  16. data/lib/generators/zuul/templates/role_subject.rb +17 -0
  17. data/lib/generators/zuul/templates/role_subject_existing.rb +24 -0
  18. data/lib/tasks/zuul.rake +56 -0
  19. data/lib/zuul.rb +14 -5
  20. data/lib/zuul/action_controller.rb +108 -0
  21. data/lib/zuul/action_controller/dsl.rb +384 -0
  22. data/lib/zuul/action_controller/evaluators.rb +60 -0
  23. data/lib/zuul/active_record.rb +338 -0
  24. data/lib/zuul/active_record/context.rb +38 -0
  25. data/lib/zuul/active_record/permission.rb +31 -0
  26. data/lib/zuul/active_record/permission_role.rb +29 -0
  27. data/lib/zuul/active_record/permission_subject.rb +29 -0
  28. data/lib/zuul/active_record/role.rb +117 -0
  29. data/lib/zuul/active_record/role_subject.rb +29 -0
  30. data/lib/zuul/active_record/scope.rb +71 -0
  31. data/lib/zuul/active_record/subject.rb +239 -0
  32. data/lib/zuul/configuration.rb +149 -0
  33. data/lib/zuul/context.rb +53 -0
  34. data/lib/zuul/exceptions.rb +3 -0
  35. data/lib/zuul/exceptions/access_denied.rb +9 -0
  36. data/lib/zuul/exceptions/invalid_context.rb +9 -0
  37. data/lib/zuul/exceptions/undefined_scope.rb +9 -0
  38. data/lib/zuul/railtie.rb +5 -0
  39. data/lib/zuul/version.rb +3 -0
  40. data/lib/zuul_viz.rb +195 -0
  41. data/spec/db/schema.rb +172 -0
  42. data/spec/spec_helper.rb +25 -0
  43. data/spec/support/capture_stdout.rb +12 -0
  44. data/spec/support/models.rb +167 -0
  45. data/spec/zuul/active_record/context_spec.rb +55 -0
  46. data/spec/zuul/active_record/permission_role_spec.rb +84 -0
  47. data/spec/zuul/active_record/permission_spec.rb +174 -0
  48. data/spec/zuul/active_record/permission_subject_spec.rb +84 -0
  49. data/spec/zuul/active_record/role_spec.rb +694 -0
  50. data/spec/zuul/active_record/role_subject_spec.rb +84 -0
  51. data/spec/zuul/active_record/scope_spec.rb +75 -0
  52. data/spec/zuul/active_record/subject_spec.rb +1186 -0
  53. data/spec/zuul/active_record_spec.rb +624 -0
  54. data/spec/zuul/configuration_spec.rb +254 -0
  55. data/spec/zuul/context_spec.rb +128 -0
  56. data/spec/zuul_spec.rb +15 -0
  57. metadata +181 -70
  58. data/.document +0 -5
  59. data/.gitignore +0 -23
  60. data/LICENSE +0 -20
  61. data/README.rdoc +0 -65
  62. data/Rakefile +0 -54
  63. data/VERSION +0 -1
  64. data/lib/zuul/restrict_access.rb +0 -104
  65. data/lib/zuul/valid_roles.rb +0 -37
  66. data/spec/rails_root/app/controllers/application_controller.rb +0 -2
  67. data/spec/rails_root/app/models/user.rb +0 -8
  68. data/spec/rails_root/config/boot.rb +0 -110
  69. data/spec/rails_root/config/database.yml +0 -5
  70. data/spec/rails_root/config/environment.rb +0 -7
  71. data/spec/rails_root/config/environments/test.rb +0 -7
  72. data/spec/rails_root/config/initializers/session_store.rb +0 -15
  73. data/spec/rails_root/config/routes.rb +0 -4
  74. data/spec/rails_root/db/test.sqlite3 +0 -0
  75. data/spec/rails_root/log/test.log +0 -5388
  76. data/spec/rails_root/spec/controllers/require_user_spec.rb +0 -138
  77. data/spec/rails_root/spec/controllers/restrict_access_spec.rb +0 -64
  78. data/spec/rails_root/spec/models/user_spec.rb +0 -37
  79. data/spec/rails_root/spec/spec_helper.rb +0 -34
  80. data/zuul.gemspec +0 -78
@@ -0,0 +1,149 @@
1
+ module Zuul
2
+ class Configuration
3
+ PRIMARY_AUTHORIZATION_CLASSES = {
4
+ :subject_class => :user,
5
+ :role_class => :role,
6
+ :permission_class => :permission
7
+ }
8
+ AUTHORIZATION_JOIN_CLASSES = {
9
+ :role_subject_class => :role_user,
10
+ :permission_role_class => :permission_role,
11
+ :permission_subject_class => :permission_user
12
+ }
13
+ DEFAULT_AUTHORIZATION_CLASSES = PRIMARY_AUTHORIZATION_CLASSES.merge(AUTHORIZATION_JOIN_CLASSES)
14
+
15
+ DEFAULT_CONFIGURATION_OPTIONS = {
16
+ :acl_default => :deny, # :allow, :deny
17
+ :acl_mode => :raise, # :raise, :quiet
18
+ :acl_collect_results => false,
19
+ :subject_method => :current_user,
20
+ :force_context => false,
21
+ :scope => :default,
22
+ :with_permissions => true
23
+ }
24
+
25
+ attr_reader *DEFAULT_AUTHORIZATION_CLASSES.keys
26
+ attr_reader *DEFAULT_CONFIGURATION_OPTIONS.keys
27
+
28
+ DEFAULT_AUTHORIZATION_CLASSES.keys.concat(DEFAULT_CONFIGURATION_OPTIONS.keys).each do |key|
29
+ define_method "#{key.to_s}=" do |val|
30
+ @changed[key] = [send(key), val]
31
+ instance_variable_set "@#{key.to_s}", val
32
+ end
33
+ end
34
+
35
+ def changed
36
+ @changed = {}
37
+ to_hash.each { |key,val| @changed[key] = [@saved_state[key], val] if @saved_state[key] != val }
38
+ @changed
39
+ end
40
+
41
+ def changed?(key)
42
+ changed.has_key?(key)
43
+ end
44
+
45
+ def configure(args={}, &block)
46
+ save_state
47
+ configure_with_args args
48
+ configure_with_block &block
49
+ configure_join_classes if PRIMARY_AUTHORIZATION_CLASSES.keys.any? { |key| changed?(key) }
50
+ self
51
+ end
52
+
53
+ def configure_with_args(args)
54
+ args.select { |k,v| DEFAULT_AUTHORIZATION_CLASSES.keys.concat(DEFAULT_CONFIGURATION_OPTIONS.keys).include?(k) }.each do |key,val|
55
+ instance_variable_set "@#{key.to_s}", val
56
+ end
57
+ end
58
+
59
+ def configure_with_block(&block)
60
+ self.instance_eval(&block) if block_given?
61
+ end
62
+
63
+ def configure_join_classes
64
+ [[:role, :subject], [:permission, :subject], [:permission, :role]].each do |join_types|
65
+ join_key = "#{join_types.sort[0].to_s}_#{join_types.sort[1].to_s}_class".to_sym
66
+ next if changed?(join_key) # don't override join table if it was provided
67
+
68
+ namespaces = []
69
+ join_class = join_types.map do |class_type|
70
+ type_class = instance_variable_get "@#{class_type.to_s}_class"
71
+ namespace = (type_class.is_a?(Class) ? type_class.name : type_class.to_s.camelize).split("::")
72
+ class_name = namespace.slice!(namespace.length-1)
73
+ namespaces << namespace.join("::") if namespace.length > 0
74
+ class_name
75
+ end.sort!.join("")
76
+
77
+ join_class = "#{namespaces[0]}::#{join_class}" if namespaces.length > 0 && namespaces.all? { |ns| ns == namespaces[0] }
78
+ instance_variable_set "@#{join_key.to_s}", join_class
79
+ end
80
+ end
81
+
82
+ def save_state
83
+ @saved_state = clone.to_hash
84
+ @changed = {}
85
+ end
86
+
87
+
88
+ def to_hash
89
+ h = {}
90
+ DEFAULT_AUTHORIZATION_CLASSES.keys.concat(DEFAULT_CONFIGURATION_OPTIONS.keys).each do |key|
91
+ h[key] = instance_variable_get "@#{key.to_s}"
92
+ end
93
+ h
94
+ end
95
+ alias_method :to_h, :to_hash
96
+
97
+ def classes
98
+ cstruct = ClassStruct.new(*DEFAULT_AUTHORIZATION_CLASSES.keys).new
99
+ DEFAULT_AUTHORIZATION_CLASSES.keys.each do |key|
100
+ cstruct.send("#{key.to_s}=", instance_variable_get("@#{key.to_s}"))
101
+ end
102
+ cstruct
103
+ end
104
+
105
+ def primary_classes
106
+ cstruct = ClassStruct.new(*PRIMARY_AUTHORIZATION_CLASSES.keys).new
107
+ PRIMARY_AUTHORIZATION_CLASSES.keys.each do |key|
108
+ cstruct.send("#{key.to_s}=", instance_variable_get("@#{key.to_s}"))
109
+ end
110
+ cstruct
111
+ end
112
+
113
+ def join_classes
114
+ cstruct = ClassStruct.new(*AUTHORIZATION_JOIN_CLASSES.keys).new
115
+ AUTHORIZATION_JOIN_CLASSES.keys.each do |key|
116
+ cstruct.send("#{key.to_s}=", instance_variable_get("@#{key.to_s}"))
117
+ end
118
+ cstruct
119
+ end
120
+
121
+ protected
122
+
123
+ def initialize
124
+ [DEFAULT_AUTHORIZATION_CLASSES, DEFAULT_CONFIGURATION_OPTIONS].each do |opts|
125
+ opts.each do |key,val|
126
+ instance_variable_set "@#{key.to_s}", val
127
+ end
128
+ end
129
+ save_state
130
+ super
131
+ end
132
+
133
+ class ClassStruct < Struct
134
+ def keys
135
+ each_pair.collect { |key,val| key }.to_a
136
+ end
137
+
138
+ def to_array
139
+ each_pair.collect { |key,val| val }.to_a
140
+ end
141
+ alias_method :to_a, :to_array
142
+
143
+ def to_hash
144
+ Hash[each_pair.to_a]
145
+ end
146
+ alias_method :to_h, :to_hash
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,53 @@
1
+ module Zuul
2
+ class Context < Struct.new(:class_name, :id)
3
+
4
+ def self.parse(*args)
5
+ if args.length >= 2
6
+ new(*args)
7
+ elsif args[0].is_a?(self)
8
+ return args[0]
9
+ elsif args[0].is_a?(Class)
10
+ new(args[0].name)
11
+ elsif args[0].class.ancestors.include?(::ActiveRecord::Base) && args[0].respond_to?(:id)
12
+ new(args[0].class.name, args[0].id)
13
+ else
14
+ new
15
+ end
16
+ end
17
+
18
+ def instance?
19
+ !class_name.nil? && !id.nil?
20
+ end
21
+ alias_method :object?, :instance?
22
+
23
+ def class?
24
+ !class_name.nil? && id.nil?
25
+ end
26
+
27
+ def nil?
28
+ class_name.nil? && id.nil?
29
+ end
30
+
31
+ def type
32
+ return :nil if class_name.nil?
33
+ return :class if id.nil?
34
+ :instance
35
+ end
36
+
37
+ def to_context
38
+ return nil if class_name.nil?
39
+ return class_name.constantize if id.nil?
40
+ class_name.constantize.find(id)
41
+ end
42
+ alias_method :context, :to_context
43
+
44
+ protected
45
+
46
+ def initialize(class_name=nil, id=nil)
47
+ raise Exceptions::InvalidContext, "Invalid Context Class" unless class_name.nil? || class_name.is_a?(String)
48
+ raise Exceptions::InvalidContext, "Invalid Context ID" unless id.nil? || id.is_a?(Integer)
49
+ raise Exceptions::InvalidContext if !id.nil? && class_name.nil?
50
+ super
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ require 'zuul/exceptions/access_denied'
2
+ require 'zuul/exceptions/invalid_context'
3
+ require 'zuul/exceptions/undefined_scope'
@@ -0,0 +1,9 @@
1
+ module Zuul
2
+ module Exceptions
3
+ class AccessDenied < StandardError
4
+ def initialize(msg = "Access Denied")
5
+ super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Zuul
2
+ module Exceptions
3
+ class InvalidContext < StandardError
4
+ def initialize(msg = "Invalid Context")
5
+ super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Zuul
2
+ module Exceptions
3
+ class UndefinedScope < StandardError
4
+ def initialize(msg = "The requested scope does not exist")
5
+ super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class ZuulTasks < Rails::Railtie
2
+ rake_tasks do
3
+ Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f }
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Zuul
2
+ VERSION = '0.2.0'
3
+ end
data/lib/zuul_viz.rb ADDED
@@ -0,0 +1,195 @@
1
+ class ZuulViz
2
+
3
+ def graph(*args)
4
+ opts = {:subject_class => Zuul.configuration.subject_class.to_s.camelize.singularize.constantize}
5
+ opts = opts.merge(args[0]) if args.length > 0
6
+ subject_class = opts[:subject_class]
7
+
8
+ g = GraphViz.new(:G, :type => :graph)
9
+ g["compound"] = true
10
+ g.edge["lhead"] = ""
11
+ g.edge["ltail"] = ""
12
+ #g["splines"] = "line"
13
+ #g["rankdir"] = "LR"
14
+ #g["nodesep"] = "2"
15
+ #g["ranksep"] = "2"
16
+
17
+ roles = subject_class.auth_scope.role_class.all
18
+ raise "No roles to graph" if roles.length == 0
19
+
20
+ ug = g.add_graph("cluster0")
21
+ ug["label"] = "Assigned #{subject_class.name.pluralize}"
22
+ rg = g.add_graph("cluster1")
23
+ rg["label"] = subject_class.auth_scope.role_class_name.pluralize
24
+
25
+ graph_roles = {}
26
+ roles.each do |role|
27
+ graph_roles[role.id] = rg.add_nodes(node_str(role))
28
+
29
+ subject_class.auth_scope.role_subject_class.where(subject_class.auth_scope.role_foreign_key.to_sym => role.id).group(:context_type, :context_id).each do |role_subject|
30
+ g.add_edges(graph_roles[role.id], ug.add_nodes("#{subject_class.auth_scope.role_subject_class.where(subject_class.auth_scope.role_foreign_key.to_sym => role.id, :context_type => role_subject.context_type, :context_id => role_subject.context_id).count} #{subject_class.name.pluralize}"), :label => context_str(role_subject.context), :color => context_color(role_subject.context), :fontcolor => context_color(role_subject.context))
31
+ end
32
+ end
33
+
34
+ permissions = subject_class.auth_scope.permission_class.all
35
+ if permissions.length > 0
36
+ graph_permissions = {}
37
+ pg = g.add_graph("cluster2")
38
+ pg["label"] = subject_class.auth_scope.permission_class_name.pluralize
39
+
40
+ permissions.each do |permission|
41
+ graph_permissions[permission.id] = pg.add_nodes(node_str(permission))
42
+
43
+ subject_class.auth_scope.permission_role_class.where(subject_class.auth_scope.permission_foreign_key.to_sym => permission.id).each do |permission_role|
44
+ g.add_edges(graph_roles[permission_role.send(subject_class.auth_scope.role_foreign_key.to_sym)], graph_permissions[permission.id], :label => context_str(permission_role.context), :color => context_color(permission_role.context), :fontcolor => context_color(permission_role.context))
45
+ end
46
+
47
+ subject_class.auth_scope.permission_subject_class.where(subject_class.auth_scope.permission_foreign_key.to_sym => permission.id).group(:context_type, :context_id).each do |permission_subject|
48
+ g.add_edges(graph_permissions[permission.id], ug.add_nodes("#{subject_class.auth_scope.permission_subject_class.where(subject_class.auth_scope.permission_foreign_key.to_sym => permission.id, :context_type => permission_subject.context_type, :context_id => permission_subject.context_id).count} #{subject_class.name.pluralize}"), :label => context_str(permission_subject.context), :color => context_color(permission_subject.context), :fontcolor => context_color(permission_subject.context))
49
+ end
50
+ end
51
+ end
52
+
53
+ g
54
+ end
55
+
56
+ def graph_subject(subject, *args)
57
+ opts = {:subject_class => Zuul.configuration.subject_class.to_s.camelize.singularize.constantize}
58
+ opts = opts.merge(args[0]) if args.length > 0
59
+ if subject.is_a?(Integer)
60
+ subject = opts[:subject_class].find(subject)
61
+ else
62
+ opts[:subject_class] = subject.class
63
+ end
64
+
65
+ g = GraphViz.new(:G, :type => :graph)
66
+ g["compound"] = true
67
+ g.edge["lhead"] = ""
68
+ g.edge["ltail"] = ""
69
+ #g["splines"] = "line"
70
+
71
+ rg = g.add_graph("cluster0")
72
+ rg["label"] = subject.auth_scope.role_class_name.pluralize
73
+ pg = g.add_graph("cluster1")
74
+ pg["label"] = subject.auth_scope.permission_class_name.pluralize
75
+
76
+ subject_node = g.add_nodes("#{subject.class.name} #{subject.id}")
77
+
78
+ subject.send(subject.auth_scope.role_subjects_table_name).each do |role_subject|
79
+ role = role_subject.send(subject.auth_scope.role_class_name.underscore)
80
+ role_node = rg.add_nodes(node_str(role))
81
+ g.add_edges(subject_node, role_node, :label => context_str(role_subject.context), :color => context_color(role_subject.context), :fontcolor => context_color(role_subject.context))
82
+
83
+ role.send(subject.auth_scope.permission_roles_table_name).each do |permission_role|
84
+ permission = permission_role.send(subject.auth_scope.permission_class_name.underscore)
85
+ g.add_edges(role_node, pg.add_nodes(node_str(permission)), :label => context_str(permission_role.context), :color => context_color(permission_role.context), :fontcolor => context_color(permission_role.context))
86
+ end
87
+ end
88
+
89
+ subject.send(subject.auth_scope.permission_subjects_table_name).each do |permission_subject|
90
+ permission = permission_subject.send(subject.auth_scope.permission_class_name.underscore)
91
+ g.add_edges(subject_node, pg.add_nodes(node_str(permission)), :label => context_str(permission_subject.context), :color => context_color(permission_subject.context), :fontcolor => context_color(permission_subject.context))
92
+ end
93
+
94
+ g
95
+ end
96
+
97
+ def graph_role(role, *args)
98
+ opts = {:role_class => Zuul.configuration.role_class.to_s.camelize.singularize.constantize}
99
+ opts = opts.merge(args[0]) if args.length > 0
100
+ if role.is_a?(Integer)
101
+ role = opts[:role_class].find(role)
102
+ else
103
+ opts[:role_class] = role.class
104
+ end
105
+
106
+ g = GraphViz.new(:G, :type => :graph)
107
+ g["compound"] = true
108
+ g.edge["lhead"] = ""
109
+ g.edge["ltail"] = ""
110
+ #g["splines"] = "line"
111
+
112
+ pg = g.add_graph("cluster0")
113
+ pg["label"] = role.auth_scope.permission_class_name.pluralize
114
+ ug = g.add_graph("cluster1")
115
+ ug["label"] = role.auth_scope.subject_class_name.pluralize
116
+
117
+ role_node = g.add_nodes(node_str(role))
118
+
119
+ role.send(role.auth_scope.permission_roles_table_name).each do |permission_role|
120
+ permission = permission_role.send(role.auth_scope.permission_class_name.underscore)
121
+ g.add_edges(role_node, pg.add_nodes(node_str(permission)), :label => context_str(permission_role.context), :color => context_color(permission_role.context), :fontcolor => context_color(permission_role.context))
122
+ end
123
+
124
+ role.auth_scope.role_subject_class.where(role.auth_scope.role_foreign_key.to_sym => role.id).group(:context_type, :context_id).each do |role_subject|
125
+ g.add_edges(role_node, ug.add_nodes("#{role.auth_scope.role_subject_class.where(role.auth_scope.role_foreign_key.to_sym => role.id, :context_type => role_subject.context_type, :context_id => role_subject.context_id).count} #{role.auth_scope.subject_class_name.pluralize}"), :label => context_str(role_subject.context), :color => context_color(role_subject.context), :fontcolor => context_color(role_subject.context))
126
+ end
127
+
128
+ g
129
+ end
130
+
131
+ def graph_permission(permission, *args)
132
+ opts = {:permission_class => Zuul.configuration.permission_class.to_s.camelize.singularize.constantize}
133
+ opts = opts.merge(args[0]) if args.length > 0
134
+ if permission.is_a?(Integer)
135
+ permission = opts[:permission_class].find(permission)
136
+ else
137
+ opts[:permission_class] = permission.class
138
+ end
139
+
140
+ g = GraphViz.new(:G, :type => :graph)
141
+ g["compound"] = true
142
+ g.edge["lhead"] = ""
143
+ g.edge["ltail"] = ""
144
+ #g["splines"] = "line"
145
+
146
+ rg = g.add_graph("cluster0")
147
+ rg["label"] = permission.auth_scope.role_class_name.pluralize
148
+ ug = g.add_graph("cluster1")
149
+ ug["label"] = permission.auth_scope.subject_class_name.pluralize
150
+
151
+ permission_node = g.add_nodes(node_str(permission))
152
+
153
+ permission.send(permission.auth_scope.permission_roles_table_name).each do |permission_role|
154
+ role = permission_role.send(permission.auth_scope.role_class_name.underscore)
155
+ g.add_edges(permission_node, rg.add_nodes(node_str(role)), :label => context_str(permission_role.context), :color => context_color(permission_role.context), :fontcolor => context_color(permission_role.context))
156
+ end
157
+
158
+ permission.auth_scope.permission_subject_class.where(permission.auth_scope.permission_foreign_key.to_sym => permission.id).group(:context_type, :context_id).each do |permission_subject|
159
+ g.add_edges(permission_node, ug.add_nodes("#{permission.auth_scope.permission_subject_class.where(permission.auth_scope.permission_foreign_key.to_sym => permission.id, :context_type => permission_subject.context_type, :context_id => permission_subject.context_id).count} #{permission.auth_scope.subject_class_name.pluralize}"), :label => context_str(permission_subject.context), :color => context_color(permission_subject.context), :fontcolor => context_color(permission_subject.context))
160
+ end
161
+
162
+ g
163
+ end
164
+
165
+ protected
166
+
167
+ def initialize
168
+ # set some configuration args here to control colors, whether edges are labeled, etc.
169
+ super
170
+ end
171
+
172
+ def context_color(context)
173
+ if context.nil?
174
+ "black"
175
+ elsif context.id.nil?
176
+ "#0000ff"
177
+ else
178
+ "#00aa00"
179
+ end
180
+ end
181
+
182
+ def context_str(context)
183
+ if context.nil?
184
+ "global"
185
+ elsif context.id.nil?
186
+ context.class_name.pluralize
187
+ else
188
+ "#{context.class_name.underscore}_#{context.id}"
189
+ end
190
+ end
191
+
192
+ def node_str(obj)
193
+ "#{obj.slug}\n#{context_str(obj.context)}"
194
+ end
195
+ end