releaf-content 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +24 -0
- data/app/assets/javascripts/releaf/controllers/releaf/content/nodes.js +88 -0
- data/app/assets/stylesheets/releaf/controllers/releaf/content/nodes.scss +234 -0
- data/app/builders/releaf/content/builders/action_dialog.rb +60 -0
- data/app/builders/releaf/content/builders/dialog.rb +15 -0
- data/app/builders/releaf/content/builders/tree.rb +84 -0
- data/app/builders/releaf/content/content_type_dialog_builder.rb +74 -0
- data/app/builders/releaf/content/copy_dialog_builder.rb +9 -0
- data/app/builders/releaf/content/go_to_dialog_builder.rb +9 -0
- data/app/builders/releaf/content/move_dialog_builder.rb +9 -0
- data/app/builders/releaf/content/nodes/content_form_builder.rb +7 -0
- data/app/builders/releaf/content/nodes/form_builder.rb +108 -0
- data/app/builders/releaf/content/nodes/index_builder.rb +24 -0
- data/app/builders/releaf/content/nodes/toolbox_builder.rb +33 -0
- data/app/controllers/releaf/content/nodes_controller.rb +166 -0
- data/app/middleware/releaf/content/routes_reloader.rb +25 -0
- data/app/validators/releaf/content/node/parent_validator.rb +48 -0
- data/app/validators/releaf/content/node/root_validator.rb +43 -0
- data/app/validators/releaf/content/node/singleness_validator.rb +102 -0
- data/app/views/releaf/content/nodes/content_type_dialog.ruby +1 -0
- data/app/views/releaf/content/nodes/copy_dialog.ruby +1 -0
- data/app/views/releaf/content/nodes/go_to_dialog.ruby +1 -0
- data/app/views/releaf/content/nodes/move_dialog.ruby +1 -0
- data/lib/releaf-content.rb +6 -0
- data/lib/releaf/content/acts_as_node.rb +73 -0
- data/lib/releaf/content/acts_as_node/action_controller/acts/node.rb +17 -0
- data/lib/releaf/content/acts_as_node/active_record/acts/node.rb +55 -0
- data/lib/releaf/content/builders_autoload.rb +18 -0
- data/lib/releaf/content/engine.rb +40 -0
- data/lib/releaf/content/node.rb +280 -0
- data/lib/releaf/content/node_mapper.rb +9 -0
- data/lib/releaf/content/route.rb +93 -0
- data/lib/releaf/content/router_proxy.rb +23 -0
- data/releaf-content.gemspec +20 -0
- data/spec/builders/content/nodes/content_form_builder_spec.rb +24 -0
- data/spec/builders/content/nodes/form_builder_spec.rb +218 -0
- data/spec/builders/content/nodes/toolbox_builder_spec.rb +108 -0
- data/spec/controllers/releaf/content/nodes_controller_spec.rb +21 -0
- data/spec/features/nodes_spec.rb +239 -0
- data/spec/lib/releaf/content/acts_as_node_spec.rb +118 -0
- data/spec/lib/releaf/content/node_spec.rb +779 -0
- data/spec/lib/releaf/content/route_spec.rb +85 -0
- data/spec/middleware/routes_reloader_spec.rb +48 -0
- data/spec/routing/node_mapper_spec.rb +142 -0
- data/spec/validators/content/node/parent_validator_spec.rb +56 -0
- data/spec/validators/content/node/root_validator_spec.rb +69 -0
- data/spec/validators/content/node/singleness_validator_spec.rb +145 -0
- metadata +145 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
module Releaf::Content
|
2
|
+
module Node
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
# TODO Node should be configurable
|
5
|
+
|
6
|
+
def locale_selection_enabled?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_content(params = {}, assignment_options = nil)
|
11
|
+
self.content = content_class.new(params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def content_class
|
15
|
+
content_type.constantize unless content_type.blank?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return node public path
|
19
|
+
def path
|
20
|
+
if parent
|
21
|
+
parent.path + "/" + slug.to_s
|
22
|
+
else
|
23
|
+
"/" + slug.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
begin
|
33
|
+
content
|
34
|
+
rescue NameError => e
|
35
|
+
raise if content_id.nil? && content_type.nil?
|
36
|
+
raise unless e.message == "uninitialized constant #{content_type}"
|
37
|
+
# Class was deleted from project.
|
38
|
+
# Lets try one more time, but this time set content_type to nil, so
|
39
|
+
# rails doesn't try to constantize it
|
40
|
+
update_columns(content_id: nil, content_type: nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
self.class.updated
|
45
|
+
end
|
46
|
+
|
47
|
+
def attributes_to_not_copy
|
48
|
+
%w[content_id depth id item_position lft rgt slug created_at updated_at]
|
49
|
+
end
|
50
|
+
|
51
|
+
def attributes_to_copy
|
52
|
+
self.class.column_names - attributes_to_not_copy
|
53
|
+
end
|
54
|
+
|
55
|
+
def copy parent_id
|
56
|
+
prevent_infinite_copy_loop(parent_id)
|
57
|
+
begin
|
58
|
+
new_node = nil
|
59
|
+
self.class.transaction do
|
60
|
+
new_node = _copy!(parent_id)
|
61
|
+
end
|
62
|
+
new_node
|
63
|
+
rescue ActiveRecord::RecordInvalid
|
64
|
+
add_error_and_raise 'descendant invalid'
|
65
|
+
else
|
66
|
+
update_settings_timestamp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def move parent_id
|
71
|
+
return if parent_id.to_i == self.parent_id
|
72
|
+
|
73
|
+
self.class.transaction do
|
74
|
+
save_under(parent_id)
|
75
|
+
descendants.each do |node|
|
76
|
+
next if node.valid?
|
77
|
+
add_error_and_raise 'descendant invalid'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# Maintain unique name within parent_id scope.
|
85
|
+
# If name is not unique add numeric postfix.
|
86
|
+
def maintain_name
|
87
|
+
postfix = nil
|
88
|
+
total_count = 0
|
89
|
+
|
90
|
+
while self.class.where(parent_id: parent_id, name: "#{name}#{postfix}").where("id != ?", id.to_i).exists? do
|
91
|
+
total_count += 1
|
92
|
+
postfix = "(#{total_count})"
|
93
|
+
end
|
94
|
+
|
95
|
+
if postfix
|
96
|
+
self.name = "#{name}#{postfix}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns closest existing locale starting from object itself
|
101
|
+
# @return [String] locale
|
102
|
+
def locale
|
103
|
+
own = super
|
104
|
+
if own
|
105
|
+
own
|
106
|
+
else
|
107
|
+
ancestors.reorder("depth DESC").
|
108
|
+
where("locale IS NOT NULL").
|
109
|
+
first.try(:locale)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Check whether object and all its ancestors are active
|
114
|
+
# @return [Boolean] returns true if object is available
|
115
|
+
def available?
|
116
|
+
# There seams to be bug in Rails 4.0.0, that prevents us from using exists?
|
117
|
+
# exists? will return nil or 1 in this query, instead of true/false (as it should)
|
118
|
+
self_and_ancestors.where(active: false).any? == false
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_error_and_raise error
|
122
|
+
errors.add(:base, error)
|
123
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
124
|
+
end
|
125
|
+
|
126
|
+
def duplicate_content
|
127
|
+
return nil if content_id.blank?
|
128
|
+
|
129
|
+
new_content = content.dup
|
130
|
+
new_content.save!
|
131
|
+
new_content
|
132
|
+
end
|
133
|
+
|
134
|
+
def copy_attributes_from node
|
135
|
+
node.attributes_to_copy.each do |attribute|
|
136
|
+
send("#{attribute}=", node.send(attribute))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def duplicate_under! parent_id
|
141
|
+
new_node = nil
|
142
|
+
self.class.transaction do
|
143
|
+
new_node = self.class.new
|
144
|
+
new_node.copy_attributes_from self
|
145
|
+
new_node.content_id = duplicate_content.try(:id)
|
146
|
+
new_node.prevent_auto_update_settings_timestamp do
|
147
|
+
new_node.save_under(parent_id)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
new_node
|
151
|
+
end
|
152
|
+
|
153
|
+
def reasign_slug
|
154
|
+
self.slug = nil
|
155
|
+
ensure_unique_url
|
156
|
+
end
|
157
|
+
|
158
|
+
def save_under target_parent_node_id
|
159
|
+
self.parent_id = target_parent_node_id
|
160
|
+
if validate_root_locale_uniqueness?
|
161
|
+
# When copying root nodes it is important to reset locale to nil.
|
162
|
+
# Later user should fill in locale. This is needed to prevent
|
163
|
+
# Rails errors about conflicting routes.
|
164
|
+
self.locale = nil
|
165
|
+
end
|
166
|
+
|
167
|
+
self.item_position = self.class.children_max_item_position(self.parent) + 1
|
168
|
+
maintain_name
|
169
|
+
reasign_slug
|
170
|
+
save!
|
171
|
+
end
|
172
|
+
|
173
|
+
def prevent_auto_update_settings_timestamp &block
|
174
|
+
original = @prevent_auto_update_settings_timestamp
|
175
|
+
@prevent_auto_update_settings_timestamp = true
|
176
|
+
yield
|
177
|
+
ensure
|
178
|
+
@prevent_auto_update_settings_timestamp = original
|
179
|
+
end
|
180
|
+
|
181
|
+
protected
|
182
|
+
|
183
|
+
def validate_root_locale_uniqueness?
|
184
|
+
locale_selection_enabled? && root?
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate_parent_node_is_not_self
|
188
|
+
return if parent_id.nil?
|
189
|
+
return if parent_id.to_i != id
|
190
|
+
self.errors.add(:parent_id, "can't be parent to itself")
|
191
|
+
end
|
192
|
+
|
193
|
+
def validate_parent_is_not_descendant
|
194
|
+
return if parent_id.nil?
|
195
|
+
return if self.descendants.find_by_id(parent_id).blank?
|
196
|
+
self.errors.add(:parent_id, "descendant can't be parent")
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def _copy! parent_id
|
202
|
+
new_node = duplicate_under! parent_id
|
203
|
+
|
204
|
+
children.each do |child|
|
205
|
+
child.send(:_copy!, new_node.id)
|
206
|
+
end
|
207
|
+
new_node
|
208
|
+
end
|
209
|
+
|
210
|
+
def prevent_infinite_copy_loop(parent_id)
|
211
|
+
return if self_and_descendants.find_by_id(parent_id).blank?
|
212
|
+
add_error_and_raise("source or descendant node can't be parent of new node")
|
213
|
+
end
|
214
|
+
|
215
|
+
def prevent_auto_update_settings_timestamp?
|
216
|
+
@prevent_auto_update_settings_timestamp == true
|
217
|
+
end
|
218
|
+
|
219
|
+
def update_settings_timestamp
|
220
|
+
self.class.updated
|
221
|
+
end
|
222
|
+
|
223
|
+
module ClassMethods
|
224
|
+
def updated_at
|
225
|
+
Releaf::Settings['releaf.content.nodes.updated_at']
|
226
|
+
end
|
227
|
+
|
228
|
+
def updated
|
229
|
+
Releaf::Settings['releaf.content.nodes.updated_at'] = Time.now
|
230
|
+
end
|
231
|
+
|
232
|
+
def children_max_item_position node
|
233
|
+
if node.nil?
|
234
|
+
roots.maximum(:item_position) || 0
|
235
|
+
else
|
236
|
+
node.children.maximum(:item_position) || 0
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def valid_node_content_class_names parent_id=nil
|
241
|
+
class_names = []
|
242
|
+
ActsAsNode.classes.each do |class_name|
|
243
|
+
test_node = self.new(content_type: class_name, parent_id: parent_id)
|
244
|
+
test_node.valid?
|
245
|
+
class_names.push class_name unless test_node.errors[:content_type].present?
|
246
|
+
end
|
247
|
+
class_names
|
248
|
+
end
|
249
|
+
|
250
|
+
def valid_node_content_classes parent_id=nil
|
251
|
+
valid_node_content_class_names(parent_id).map(&:constantize)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
included do
|
256
|
+
acts_as_nested_set order_column: :item_position
|
257
|
+
acts_as_list scope: :parent_id, column: :item_position
|
258
|
+
|
259
|
+
default_scope { order(:item_position) }
|
260
|
+
scope :active, ->() { where(active: true) }
|
261
|
+
|
262
|
+
validates_presence_of :name, :slug, :content_type
|
263
|
+
validates_uniqueness_of :slug, scope: :parent_id
|
264
|
+
validates_length_of :name, :slug, :content_type, maximum: 255
|
265
|
+
validates_uniqueness_of :locale, scope: :parent_id, if: :validate_root_locale_uniqueness?
|
266
|
+
validates_presence_of :parent, if: :parent_id?
|
267
|
+
validate :validate_parent_node_is_not_self
|
268
|
+
validate :validate_parent_is_not_descendant
|
269
|
+
|
270
|
+
alias_attribute :to_text, :name
|
271
|
+
|
272
|
+
belongs_to :content, polymorphic: true, dependent: :destroy
|
273
|
+
accepts_nested_attributes_for :content
|
274
|
+
|
275
|
+
after_save :update_settings_timestamp, unless: :prevent_auto_update_settings_timestamp?
|
276
|
+
|
277
|
+
acts_as_url :name, url_attribute: :slug, scope: :parent_id, :only_when_blank => true
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Releaf::Content
|
2
|
+
module NodeMapper
|
3
|
+
def releaf_routes_for(node_class, controller: Releaf::Content::Route.node_class_default_controller(node_class), &block)
|
4
|
+
Releaf::Content::Route.for(node_class, controller).each do |route|
|
5
|
+
Releaf::Content::RouterProxy.new(self, route).draw(&block)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Releaf::Content
|
2
|
+
class Route
|
3
|
+
attr_accessor :path, :node, :locale, :node_id, :default_controller
|
4
|
+
|
5
|
+
def self.node_class
|
6
|
+
# TODO model should be configurable
|
7
|
+
::Node
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.node_class_default_controller(node_class)
|
11
|
+
if node_class <= ActionController::Base
|
12
|
+
node_class.name.underscore.sub(/_controller$/, '')
|
13
|
+
else
|
14
|
+
node_class.name.pluralize.underscore
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return node route params which can be used in Rails route options
|
19
|
+
#
|
20
|
+
# @param method_or_path [String] string with action and controller for route (Ex. home#index)
|
21
|
+
# @param options [Hash] options to merge with internally built params. Passed params overrides route params.
|
22
|
+
# @return [Hash] route options. Will return at least node "node_id" and "locale" keys.
|
23
|
+
def params(method_or_path, options = {})
|
24
|
+
method_or_path = method_or_path.to_s
|
25
|
+
action_path = path_for(method_or_path, options)
|
26
|
+
options[:to] = controller_and_action_for(method_or_path, options)
|
27
|
+
|
28
|
+
route_options = options.merge({
|
29
|
+
node_id: node_id.to_s,
|
30
|
+
locale: locale,
|
31
|
+
})
|
32
|
+
|
33
|
+
# normalize as with locale
|
34
|
+
if locale.present? && route_options[:as].present?
|
35
|
+
route_options[:as] = "#{locale}_#{route_options[:as]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
[action_path, route_options]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return routes for given class that implement ActsAsNode
|
42
|
+
#
|
43
|
+
# @param class_name [Class] class name to load related nodes
|
44
|
+
# @param default_controller [String]
|
45
|
+
# @return [Array] array of Content::Route objects
|
46
|
+
def self.for(content_type, default_controller)
|
47
|
+
node_class.where(content_type: content_type).each.inject([]) do |routes, node|
|
48
|
+
routes << build_route_object(node, default_controller) if node.available?
|
49
|
+
routes
|
50
|
+
end
|
51
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Build Content::Route from Node object
|
56
|
+
def self.build_route_object(node, default_controller)
|
57
|
+
route = new
|
58
|
+
route.node_id = node.id.to_s
|
59
|
+
route.path = node.path
|
60
|
+
route.locale = node.root.locale
|
61
|
+
route.default_controller = default_controller
|
62
|
+
|
63
|
+
route
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def path_for(method_or_path, options)
|
69
|
+
if method_or_path.include?('#')
|
70
|
+
path
|
71
|
+
elsif options.key?(:to)
|
72
|
+
"#{path}/#{method_or_path}"
|
73
|
+
else
|
74
|
+
path
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def controller_and_action_for(method_or_path, options)
|
79
|
+
if method_or_path.start_with?('#')
|
80
|
+
"#{default_controller}#{method_or_path}"
|
81
|
+
elsif method_or_path.include?('#')
|
82
|
+
method_or_path
|
83
|
+
elsif options[:to].try!(:start_with?, '#')
|
84
|
+
"#{default_controller}#{options[:to]}"
|
85
|
+
elsif options[:to].try!(:include?, '#')
|
86
|
+
options[:to]
|
87
|
+
else
|
88
|
+
"#{default_controller}##{method_or_path}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Releaf::Content
|
2
|
+
class RouterProxy
|
3
|
+
attr_accessor :router, :releaf_route
|
4
|
+
|
5
|
+
def initialize(router, releaf_route)
|
6
|
+
self.router = router
|
7
|
+
self.releaf_route = releaf_route
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw(&block)
|
11
|
+
instance_exec(releaf_route, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method_name, *args, &block)
|
15
|
+
if router.respond_to?(method_name)
|
16
|
+
router.public_send(method_name, *releaf_route.params(*args), &block)
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path("../../releaf-core/lib/releaf/version.rb", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "releaf-content"
|
5
|
+
s.version = Releaf::VERSION
|
6
|
+
|
7
|
+
s.summary = "Node and content routes support for releaf"
|
8
|
+
s.description = "Content subsystem for releaf"
|
9
|
+
s.authors = ["CubeSystems"]
|
10
|
+
s.email = 'info@cubesystems.lv'
|
11
|
+
s.homepage = 'https://github.com/cubesystems/releaf'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = Dir["spec/**/*"]
|
15
|
+
|
16
|
+
s.add_dependency 'releaf-core', Releaf::VERSION
|
17
|
+
s.add_dependency 'stringex'
|
18
|
+
s.add_dependency 'awesome_nested_set'
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rails_helper"
|
2
|
+
|
3
|
+
describe Releaf::Content::Nodes::ContentFormBuilder, type: :class do
|
4
|
+
class FormBuilderTestHelper < ActionView::Base
|
5
|
+
include Releaf::ApplicationHelper
|
6
|
+
include Releaf::ButtonHelper
|
7
|
+
include FontAwesome::Rails::IconHelper
|
8
|
+
def controller_scope_name; end
|
9
|
+
def generate_url_releaf_content_nodes_path(args); end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:template){ FormBuilderTestHelper.new }
|
13
|
+
let(:node){ Node.new(content_type: "TextPage", slug: "b", id: 2,
|
14
|
+
parent: Node.new(content_type: "TextPage", slug: "a", id: 1)) }
|
15
|
+
let(:object){ node.build_content }
|
16
|
+
let(:subject){ described_class.new(:resource, object, template, {}) }
|
17
|
+
|
18
|
+
describe "#field_names" do
|
19
|
+
it "returns array of node content object fields" do
|
20
|
+
allow(object.class).to receive(:acts_as_node_fields).and_return(["a", "b"])
|
21
|
+
expect(subject.field_names).to eq(["a", "b"])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|