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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +24 -0
  3. data/app/assets/javascripts/releaf/controllers/releaf/content/nodes.js +88 -0
  4. data/app/assets/stylesheets/releaf/controllers/releaf/content/nodes.scss +234 -0
  5. data/app/builders/releaf/content/builders/action_dialog.rb +60 -0
  6. data/app/builders/releaf/content/builders/dialog.rb +15 -0
  7. data/app/builders/releaf/content/builders/tree.rb +84 -0
  8. data/app/builders/releaf/content/content_type_dialog_builder.rb +74 -0
  9. data/app/builders/releaf/content/copy_dialog_builder.rb +9 -0
  10. data/app/builders/releaf/content/go_to_dialog_builder.rb +9 -0
  11. data/app/builders/releaf/content/move_dialog_builder.rb +9 -0
  12. data/app/builders/releaf/content/nodes/content_form_builder.rb +7 -0
  13. data/app/builders/releaf/content/nodes/form_builder.rb +108 -0
  14. data/app/builders/releaf/content/nodes/index_builder.rb +24 -0
  15. data/app/builders/releaf/content/nodes/toolbox_builder.rb +33 -0
  16. data/app/controllers/releaf/content/nodes_controller.rb +166 -0
  17. data/app/middleware/releaf/content/routes_reloader.rb +25 -0
  18. data/app/validators/releaf/content/node/parent_validator.rb +48 -0
  19. data/app/validators/releaf/content/node/root_validator.rb +43 -0
  20. data/app/validators/releaf/content/node/singleness_validator.rb +102 -0
  21. data/app/views/releaf/content/nodes/content_type_dialog.ruby +1 -0
  22. data/app/views/releaf/content/nodes/copy_dialog.ruby +1 -0
  23. data/app/views/releaf/content/nodes/go_to_dialog.ruby +1 -0
  24. data/app/views/releaf/content/nodes/move_dialog.ruby +1 -0
  25. data/lib/releaf-content.rb +6 -0
  26. data/lib/releaf/content/acts_as_node.rb +73 -0
  27. data/lib/releaf/content/acts_as_node/action_controller/acts/node.rb +17 -0
  28. data/lib/releaf/content/acts_as_node/active_record/acts/node.rb +55 -0
  29. data/lib/releaf/content/builders_autoload.rb +18 -0
  30. data/lib/releaf/content/engine.rb +40 -0
  31. data/lib/releaf/content/node.rb +280 -0
  32. data/lib/releaf/content/node_mapper.rb +9 -0
  33. data/lib/releaf/content/route.rb +93 -0
  34. data/lib/releaf/content/router_proxy.rb +23 -0
  35. data/releaf-content.gemspec +20 -0
  36. data/spec/builders/content/nodes/content_form_builder_spec.rb +24 -0
  37. data/spec/builders/content/nodes/form_builder_spec.rb +218 -0
  38. data/spec/builders/content/nodes/toolbox_builder_spec.rb +108 -0
  39. data/spec/controllers/releaf/content/nodes_controller_spec.rb +21 -0
  40. data/spec/features/nodes_spec.rb +239 -0
  41. data/spec/lib/releaf/content/acts_as_node_spec.rb +118 -0
  42. data/spec/lib/releaf/content/node_spec.rb +779 -0
  43. data/spec/lib/releaf/content/route_spec.rb +85 -0
  44. data/spec/middleware/routes_reloader_spec.rb +48 -0
  45. data/spec/routing/node_mapper_spec.rb +142 -0
  46. data/spec/validators/content/node/parent_validator_spec.rb +56 -0
  47. data/spec/validators/content/node/root_validator_spec.rb +69 -0
  48. data/spec/validators/content/node/singleness_validator_spec.rb +145 -0
  49. 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