releaf-content 0.2.1 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +19 -21
  3. data/app/assets/javascripts/{releaf/controllers → controllers}/releaf/content/nodes.js +0 -0
  4. data/app/assets/stylesheets/{releaf/controllers → controllers}/releaf/content/nodes.scss +0 -0
  5. data/app/builders/releaf/content/builders/action_dialog.rb +9 -1
  6. data/app/builders/releaf/content/builders/tree.rb +1 -1
  7. data/app/builders/releaf/content/content_type_dialog_builder.rb +2 -2
  8. data/app/builders/releaf/content/nodes/form_builder.rb +3 -3
  9. data/app/builders/releaf/content/nodes/index_builder.rb +1 -1
  10. data/app/builders/releaf/content/nodes/toolbox_builder.rb +0 -5
  11. data/app/controllers/releaf/content/nodes_controller.rb +109 -129
  12. data/app/middleware/releaf/content/routes_reloader.rb +12 -4
  13. data/app/services/releaf/content/node/copy.rb +90 -0
  14. data/app/services/releaf/content/node/move.rb +21 -0
  15. data/app/services/releaf/content/node/save_under_parent.rb +22 -0
  16. data/app/services/releaf/content/node/service.rb +17 -0
  17. data/app/validators/releaf/content/node/singleness_validator.rb +1 -24
  18. data/lib/releaf-content.rb +85 -6
  19. data/lib/releaf/content/acts_as_node.rb +0 -5
  20. data/lib/releaf/content/acts_as_node/active_record/acts/node.rb +2 -8
  21. data/lib/releaf/content/configuration.rb +61 -0
  22. data/lib/releaf/content/engine.rb +1 -34
  23. data/lib/releaf/content/node.rb +30 -101
  24. data/lib/releaf/content/node_mapper.rb +28 -2
  25. data/lib/releaf/content/route.rb +37 -25
  26. data/spec/builders/content/nodes/action_dialog_spec.rb +39 -0
  27. data/spec/builders/content/nodes/form_builder_spec.rb +47 -3
  28. data/spec/builders/content/nodes/toolbox_builder_spec.rb +1 -32
  29. data/spec/controllers/releaf/content/nodes_controller_spec.rb +42 -11
  30. data/spec/features/nodes_services_spec.rb +207 -0
  31. data/spec/features/nodes_spec.rb +328 -30
  32. data/spec/lib/releaf/content/acts_as_node_spec.rb +4 -32
  33. data/spec/lib/releaf/content/configuration_spec.rb +159 -0
  34. data/spec/lib/releaf/content/engine_spec.rb +149 -0
  35. data/spec/lib/releaf/content/node_spec.rb +66 -324
  36. data/spec/lib/releaf/content/route_spec.rb +223 -34
  37. data/spec/middleware/routes_reloader_spec.rb +62 -14
  38. data/spec/routing/node_mapper_spec.rb +223 -55
  39. data/spec/services/releaf/content/node/copy_spec.rb +115 -0
  40. data/spec/services/releaf/content/node/move_spec.rb +20 -0
  41. data/spec/services/releaf/content/node/save_under_parent_spec.rb +49 -0
  42. data/spec/services/releaf/content/node/service_spec.rb +19 -0
  43. metadata +38 -21
  44. data/app/builders/releaf/content/go_to_dialog_builder.rb +0 -9
  45. data/app/views/releaf/content/nodes/go_to_dialog.ruby +0 -1
  46. data/lib/releaf/content/builders_autoload.rb +0 -18
  47. data/releaf-content.gemspec +0 -20
@@ -0,0 +1,90 @@
1
+ module Releaf
2
+ module Content::Node
3
+ class Copy
4
+ include Releaf::Content::Node::Service
5
+ attribute :parent_id, Integer, strict: false
6
+
7
+ def call
8
+ prevent_infinite_copy_loop
9
+ begin
10
+ new_node = nil
11
+ node.class.transaction do
12
+ new_node = make_copy
13
+ end
14
+ new_node
15
+ rescue ActiveRecord::RecordInvalid
16
+ add_error_and_raise 'descendant invalid'
17
+ else
18
+ node.update_settings_timestamp
19
+ end
20
+ end
21
+
22
+ def prevent_infinite_copy_loop
23
+ return if node.self_and_descendants.find_by_id(parent_id).blank?
24
+ add_error_and_raise("source or descendant node can't be parent of new node")
25
+ end
26
+
27
+ def duplicate_under
28
+ new_node = nil
29
+ node.class.transaction do
30
+ new_node = node.class.new
31
+ new_node.assign_attributes_from(node)
32
+ new_node.content_id = duplicate_content.try(:id)
33
+ new_node.prevent_auto_update_settings_timestamp do
34
+ Releaf::Content::Node::SaveUnderParent.call(node: new_node, parent_id: parent_id)
35
+ end
36
+ end
37
+ new_node
38
+ end
39
+
40
+ def duplicate_content
41
+ return if node.content_id.blank?
42
+
43
+ new_content = node.content.class.new(cloned_content_attributes)
44
+ duplicate_content_dragonfly_attributes(new_content)
45
+
46
+ new_content.save!
47
+ new_content
48
+ end
49
+
50
+ def cloned_content_attributes
51
+ skippable_attribute_names = ["id"] + content_dragonfly_attributes
52
+ node.content.attributes.except(*skippable_attribute_names)
53
+ end
54
+
55
+ def duplicate_content_dragonfly_attributes(new_content)
56
+ content_dragonfly_attributes.each do |attribute_name|
57
+ accessor_name = attribute_name.gsub("_uid", "")
58
+ dragonfly_attachment = node.content.send(accessor_name)
59
+
60
+ if dragonfly_attachment.present?
61
+ begin
62
+ dragonfly_attachment.path # verify that the file exists
63
+ rescue Dragonfly::Job::Fetch::NotFound
64
+ dragonfly_attachment = nil
65
+ end
66
+ end
67
+
68
+ new_content.send("#{attribute_name}=", nil)
69
+ new_content.send("#{accessor_name}=", dragonfly_attachment)
70
+ end
71
+ end
72
+
73
+ def content_dragonfly_attributes
74
+ node.content.class.attribute_names.select do |attribute_name|
75
+ Releaf::Builders::Utilities::ResolveAttributeFieldMethodName.new(object: node.content, attribute_name: attribute_name).file?
76
+ end
77
+ end
78
+
79
+ def make_copy
80
+ new_node = duplicate_under
81
+
82
+ node.children.each do |child|
83
+ self.class.new(node: child, parent_id: new_node.id).make_copy
84
+ end
85
+
86
+ new_node
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,21 @@
1
+ module Releaf
2
+ module Content::Node
3
+ class Move
4
+ include Releaf::Content::Node::Service
5
+ attribute :parent_id, Integer, strict: false
6
+
7
+ def call
8
+ return if node.parent_id.to_i == parent_id
9
+
10
+ node.class.transaction do
11
+ Releaf::Content::Node::SaveUnderParent.call(node: node, parent_id: parent_id)
12
+
13
+ node.descendants.each do |descendant_node|
14
+ next if descendant_node.valid?
15
+ add_error_and_raise("descendant invalid")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ module Releaf
2
+ module Content::Node
3
+ class SaveUnderParent
4
+ include Releaf::Content::Node::Service
5
+ attribute :parent_id, Integer, strict: false
6
+
7
+ def call
8
+ node.parent_id = parent_id
9
+ if node.validate_root_locale_uniqueness?
10
+ # When copying root nodes it is important to reset locale to nil.
11
+ # Later user should fill in locale. This is needed to prevent
12
+ # Rails errors about conflicting routes.
13
+ node.locale = nil
14
+ end
15
+
16
+ node.maintain_name
17
+ node.reasign_slug
18
+ node.save!
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Releaf
2
+ module Content::Node
3
+ module Service
4
+ extend ActiveSupport::Concern
5
+ include Releaf::Service
6
+
7
+ included do
8
+ attribute :node, Releaf::Content::Node
9
+ end
10
+
11
+ def add_error_and_raise(error)
12
+ node.errors.add(:base, error)
13
+ raise ActiveRecord::RecordInvalid.new(node)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,28 +1,5 @@
1
1
  module Releaf
2
2
  module Content::Node
3
- # Validator to test if node is valid, when created under given parrent node
4
- #
5
- # validator needs :for and :under options, both can be either array of classes
6
- # or single class.
7
- #
8
- # :for option specifies for which nodes validation should be applied
9
- #
10
- # :under option specifies under which nodes only single node of given type
11
- # should be valid.
12
- # If under is unspecified, then only single node with given class can be
13
- # created under entrie content tree.
14
- #
15
- # @example
16
- #
17
- # class Node < ActiveRecord::Base
18
- # includes Releaf::Content::Node
19
- # validates_with Releaf::Content::Node::ParentValidator, for: [Text, Book], under: Store
20
- # end
21
- #
22
- # In the example above, Text node or book node can be added in any leve
23
- # under Store node, but only once.
24
- # However they can be added to any other node as many times as needed, as
25
- # long as none of ancestors are Store node.
26
3
  class SinglenessValidator < ActiveModel::Validator
27
4
 
28
5
  def validate node
@@ -67,7 +44,7 @@ module Releaf
67
44
  # return some other parent, than @node.parent.ancestors, even though
68
45
  # both return same node.
69
46
  # Seams like a bug in AwesomeNestedSet (in case of @node.parent.ancestors).
70
- parent_node = Node.find(@node.parent_id)
47
+ parent_node = @node.class.find(@node.parent_id)
71
48
 
72
49
  ancestor_node = parent_node.self_and_ancestors.where(content_type: ancestor_class_names).reorder(:depth).last
73
50
  if ancestor_node.nil?
@@ -1,6 +1,85 @@
1
- require 'releaf/content/router_proxy'
2
- require 'releaf/content/node_mapper'
3
- require 'releaf/content/engine'
4
- require 'releaf/content/acts_as_node'
5
- require 'releaf/content/node'
6
- require 'releaf/content/route'
1
+ require 'awesome_nested_set'
2
+ require 'stringex'
3
+
4
+ module Releaf::Content
5
+ require 'releaf/content/engine'
6
+ require 'releaf/content/configuration'
7
+ require 'releaf/content/router_proxy'
8
+ require 'releaf/content/node_mapper'
9
+ require 'releaf/content/acts_as_node'
10
+ require 'releaf/content/node'
11
+ require 'releaf/content/route'
12
+
13
+ # expose configuration wrapper methods as class methods for easier access
14
+ # so that, for example,
15
+ # Releaf::Content.models
16
+ # can be used instead of
17
+ # Releaf::Content.configuration.models
18
+ class << self
19
+ delegate :resources, :models, :default_model, :controllers, :routing, to: :configuration
20
+ end
21
+
22
+ def self.configuration
23
+ Releaf.application.config.content
24
+ end
25
+
26
+ def self.configure_component
27
+ Releaf.application.config.add_configuration(
28
+ Releaf::Content::Configuration.new(
29
+ resources: { 'Node' => { controller: 'Releaf::Content::NodesController' } }
30
+ )
31
+ )
32
+ end
33
+
34
+ def self.initialize_component
35
+ Rails.application.config.middleware.use Releaf::Content::RoutesReloader
36
+
37
+ ActiveSupport.on_load :action_controller do
38
+ ActionDispatch::Routing::Mapper.send(:include, Releaf::Content::NodeMapper)
39
+ end
40
+ end
41
+
42
+ def self.draw_component_routes(router)
43
+ resources.each do |_model_name, options|
44
+ draw_resource_routes(router, options)
45
+ end
46
+ end
47
+
48
+ def self.draw_resource_routes router, options
49
+ route_params = resource_route_params options
50
+
51
+ router.releaf_resources(*route_params) do
52
+ router.collection do
53
+ router.get :content_type_dialog
54
+ router.get :generate_url
55
+ end
56
+
57
+ router.member do
58
+ router.get :copy_dialog
59
+ router.post :copy
60
+ router.get :move_dialog
61
+ router.post :move
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.resource_route_params options
67
+ # Releaf::Content::NodesController -> releaf/content/nodes
68
+ controller_path = options[:controller].constantize.controller_path
69
+ controller_path_parts = controller_path.split('/')
70
+
71
+ resources_name = controller_path_parts.last
72
+
73
+ route_options = {
74
+ # releaf/content/nodes -> releaf/content
75
+ module: controller_path_parts.slice(0...-1).join('/'),
76
+
77
+ # releaf/content/nodes -> releaf_content_nodes
78
+ as: controller_path.gsub(/\//, '_'),
79
+
80
+ except: [:show]
81
+ }
82
+
83
+ [ resources_name, route_options ]
84
+ end
85
+ end
@@ -10,11 +10,6 @@ module ActsAsNode
10
10
 
11
11
  module ClassMethods
12
12
 
13
- # Load all nodes for class
14
- def nodes
15
- ::Node.where(content_type: self.name)
16
- end
17
-
18
13
  # There are no configuration options yet.
19
14
  #
20
15
  def acts_as_node(params: nil, fields: nil)
@@ -22,7 +22,7 @@ module ActiveRecord
22
22
 
23
23
  def acts_as_node_params
24
24
  if acts_as_node_configuration[:params].nil?
25
- Releaf::Core::ResourceParams.new(self).values << :id
25
+ Releaf::ResourceParams.new(self).values << :id
26
26
  else
27
27
  acts_as_node_configuration[:params] << :id
28
28
  end
@@ -33,7 +33,7 @@ module ActiveRecord
33
33
  # @return [Array] list of fields to display
34
34
  def acts_as_node_fields
35
35
  if acts_as_node_configuration[:fields].nil?
36
- Releaf::Core::ResourceFields.new(self).values
36
+ Releaf::ResourceFields.new(self).values
37
37
  else
38
38
  acts_as_node_configuration[:fields]
39
39
  end
@@ -43,12 +43,6 @@ module ActiveRecord
43
43
  # All the methods available to a record that has had <tt>acts_as_node</tt> specified.
44
44
  module InstanceMethods
45
45
 
46
- # Return object corresponding node object
47
- # @return [::Node]
48
- def node
49
- ::Node.find_by(content_type: self.class.name, content_id: id)
50
- end
51
-
52
46
  end
53
47
  end
54
48
  end
@@ -0,0 +1,61 @@
1
+ module Releaf::Content
2
+ class Configuration
3
+ include Virtus.model(strict: true)
4
+ attribute :resources, Hash
5
+
6
+ def resources=(value)
7
+ verify_resources_config(value)
8
+ super
9
+ end
10
+
11
+ def verify_resources_config(resource_config)
12
+ # perform some basic config structure validation
13
+ unless resource_config.is_a? Hash
14
+ raise Releaf::Error, "Releaf.application.config.content.resources must be a Hash"
15
+ end
16
+
17
+ resource_config.each do | key, values |
18
+ unless key.is_a? String
19
+ raise Releaf::Error, "Releaf.application.config.content.resources must have string keys"
20
+ end
21
+ unless values.is_a? Hash
22
+ raise Releaf::Error, "#{key} in Releaf.application.config.content.resources must have a hash value"
23
+ end
24
+ unless values[:controller].is_a? String
25
+ raise Releaf::Error, "#{key} in Releaf.application.config.content.resources must have controller class specified as a string"
26
+ end
27
+ end
28
+ end
29
+
30
+ def models
31
+ model_names.map(&:constantize)
32
+ end
33
+
34
+ def model_names
35
+ @model_names ||= resources.keys
36
+ end
37
+
38
+ def default_model
39
+ models.first
40
+ end
41
+
42
+ def controllers
43
+ controller_names.map(&:constantize)
44
+ end
45
+
46
+ def controller_names
47
+ @controller_names ||= resources.values.map { |options| options[:controller] }
48
+ end
49
+
50
+ def routing
51
+ @routing ||= resources.map do | node_class_name, options |
52
+ routing = options[:routing] || {}
53
+ routing[:site] ||= nil
54
+ routing[:constraints] ||= nil
55
+ [ node_class_name, routing ]
56
+ end.to_h
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -1,40 +1,7 @@
1
- require 'awesome_nested_set'
2
- require 'stringex'
3
-
4
1
  module Releaf::Content
5
- require 'releaf/content/builders_autoload'
6
2
  class Engine < ::Rails::Engine
7
3
  initializer 'precompile', group: :all do |app|
8
- app.config.assets.precompile += %w(releaf/controllers/releaf/content/*)
4
+ app.config.assets.precompile += %w(controllers/releaf/content/*)
9
5
  end
10
6
  end
11
-
12
- def self.initialize_component
13
- Rails.application.config.middleware.use Releaf::Content::RoutesReloader
14
- end
15
-
16
- def self.draw_component_routes router
17
- router.namespace :releaf, path: nil do
18
- router.namespace :content, path: nil do
19
- router.releaf_resources :nodes, except: [:show] do
20
- router.collection do
21
- router.get :content_type_dialog
22
- router.get :generate_url
23
- router.get :go_to_dialog
24
- end
25
-
26
- router.member do
27
- router.get :copy_dialog
28
- router.post :copy
29
- router.get :move_dialog
30
- router.post :move
31
- end
32
- end
33
- end
34
- end
35
- end
36
-
37
- ActiveSupport.on_load :action_controller do
38
- ActionDispatch::Routing::Mapper.send(:include, Releaf::Content::NodeMapper)
39
- end
40
7
  end
@@ -1,13 +1,12 @@
1
1
  module Releaf::Content
2
2
  module Node
3
3
  extend ActiveSupport::Concern
4
- # TODO Node should be configurable
5
4
 
6
5
  def locale_selection_enabled?
7
6
  false
8
7
  end
9
8
 
10
- def build_content(params = {}, assignment_options = nil)
9
+ def build_content(params = {})
11
10
  self.content = content_class.new(params)
12
11
  end
13
12
 
@@ -17,11 +16,18 @@ module Releaf::Content
17
16
 
18
17
  # Return node public path
19
18
  def path
20
- if parent
21
- parent.path + "/" + slug.to_s
22
- else
23
- "/" + slug.to_s
24
- end
19
+ "/" + path_parts.join("/") + (trailing_slash_for_path? ? "/" : "")
20
+ end
21
+
22
+ def path_parts
23
+ list = []
24
+ list += parent.path_parts if parent
25
+ list << slug.to_s
26
+ list
27
+ end
28
+
29
+ def trailing_slash_for_path?
30
+ Rails.application.routes.default_url_options[:trailing_slash] == true
25
31
  end
26
32
 
27
33
  def to_s
@@ -45,40 +51,21 @@ module Releaf::Content
45
51
  end
46
52
 
47
53
  def attributes_to_not_copy
48
- %w[content_id depth id item_position lft rgt slug created_at updated_at]
54
+ list = %w[content_id depth id item_position lft rgt slug created_at updated_at]
55
+ list << "locale" if locale_before_type_cast.blank?
56
+ list
49
57
  end
50
58
 
51
59
  def attributes_to_copy
52
60
  self.class.column_names - attributes_to_not_copy
53
61
  end
54
62
 
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
63
+ def copy(parent_id)
64
+ Releaf::Content::Node::Copy.call(node: self, parent_id: parent_id)
68
65
  end
69
66
 
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
67
+ def move(parent_id)
68
+ Releaf::Content::Node::Move.call(node: self, parent_id: parent_id)
82
69
  end
83
70
 
84
71
  # Maintain unique name within parent_id scope.
@@ -118,59 +105,18 @@ module Releaf::Content
118
105
  self_and_ancestors.where(active: false).any? == false
119
106
  end
120
107
 
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
108
  def reasign_slug
154
109
  self.slug = nil
155
110
  ensure_unique_url
156
111
  end
157
112
 
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
113
+ def assign_attributes_from(source_node)
114
+ source_node.attributes_to_copy.each do |attribute|
115
+ send("#{attribute}=", source_node.send(attribute))
165
116
  end
166
-
167
- self.item_position = self.class.children_max_item_position(self.parent) + 1
168
- maintain_name
169
- reasign_slug
170
- save!
171
117
  end
172
118
 
173
- def prevent_auto_update_settings_timestamp &block
119
+ def prevent_auto_update_settings_timestamp
174
120
  original = @prevent_auto_update_settings_timestamp
175
121
  @prevent_auto_update_settings_timestamp = true
176
122
  yield
@@ -178,12 +124,16 @@ module Releaf::Content
178
124
  @prevent_auto_update_settings_timestamp = original
179
125
  end
180
126
 
181
- protected
127
+ def update_settings_timestamp
128
+ self.class.updated
129
+ end
182
130
 
183
131
  def validate_root_locale_uniqueness?
184
132
  locale_selection_enabled? && root?
185
133
  end
186
134
 
135
+ protected
136
+
187
137
  def validate_parent_node_is_not_self
188
138
  return if parent_id.nil?
189
139
  return if parent_id.to_i != id
@@ -198,28 +148,10 @@ module Releaf::Content
198
148
 
199
149
  private
200
150
 
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
151
  def prevent_auto_update_settings_timestamp?
216
152
  @prevent_auto_update_settings_timestamp == true
217
153
  end
218
154
 
219
- def update_settings_timestamp
220
- self.class.updated
221
- end
222
-
223
155
  module ClassMethods
224
156
  def updated_at
225
157
  Releaf::Settings['releaf.content.nodes.updated_at']
@@ -254,7 +186,7 @@ module Releaf::Content
254
186
 
255
187
  included do
256
188
  acts_as_nested_set order_column: :item_position
257
- acts_as_list scope: :parent_id, column: :item_position
189
+ acts_as_list scope: :parent_id, column: :item_position, add_new_at: :bottom
258
190
 
259
191
  default_scope { order(:item_position) }
260
192
  scope :active, ->() { where(active: true) }
@@ -266,9 +198,6 @@ module Releaf::Content
266
198
  validates_presence_of :parent, if: :parent_id?
267
199
  validate :validate_parent_node_is_not_self
268
200
  validate :validate_parent_is_not_descendant
269
-
270
- alias_attribute :to_text, :name
271
-
272
201
  belongs_to :content, polymorphic: true, dependent: :destroy
273
202
  accepts_nested_attributes_for :content
274
203