releaf-content 0.2.1
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/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
|