releaf-content 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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