dead_simple_cms 0.9.0
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.
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +266 -0
- data/Rakefile +17 -0
- data/dead_simple_cms.gemspec +47 -0
- data/lib/dead_simple_cms.rb +132 -0
- data/lib/dead_simple_cms/attribute/collection.rb +45 -0
- data/lib/dead_simple_cms/attribute/type/all.rb +104 -0
- data/lib/dead_simple_cms/attribute/type/base.rb +79 -0
- data/lib/dead_simple_cms/attribute/type/collection_support.rb +26 -0
- data/lib/dead_simple_cms/configuration.rb +50 -0
- data/lib/dead_simple_cms/file_uploader/base.rb +29 -0
- data/lib/dead_simple_cms/group.rb +58 -0
- data/lib/dead_simple_cms/group/configuration.rb +32 -0
- data/lib/dead_simple_cms/group/presenter.rb +23 -0
- data/lib/dead_simple_cms/rails/action_controller/extensions.rb +24 -0
- data/lib/dead_simple_cms/rails/action_controller/fragment_sweeper.rb +19 -0
- data/lib/dead_simple_cms/rails/action_view/extensions.rb +14 -0
- data/lib/dead_simple_cms/rails/action_view/form_builders/default.rb +16 -0
- data/lib/dead_simple_cms/rails/action_view/form_builders/interface.rb +72 -0
- data/lib/dead_simple_cms/rails/action_view/form_builders/simple_form.rb +43 -0
- data/lib/dead_simple_cms/rails/action_view/form_builders/simple_form_with_bootstrap.rb +19 -0
- data/lib/dead_simple_cms/rails/action_view/presenter.rb +63 -0
- data/lib/dead_simple_cms/section.rb +103 -0
- data/lib/dead_simple_cms/section/builder.rb +73 -0
- data/lib/dead_simple_cms/storage/base.rb +49 -0
- data/lib/dead_simple_cms/storage/database.rb +33 -0
- data/lib/dead_simple_cms/storage/memory.rb +20 -0
- data/lib/dead_simple_cms/storage/rails_cache.rb +19 -0
- data/lib/dead_simple_cms/storage/redis.rb +23 -0
- data/lib/dead_simple_cms/util/identifier.rb +40 -0
- data/lib/dead_simple_cms/util/identifier/dictionary.rb +65 -0
- data/lib/dead_simple_cms/version.rb +3 -0
- data/spec/dead_simple_cms/attribute/collection_spec.rb +46 -0
- data/spec/dead_simple_cms/attribute/type/all_spec.rb +252 -0
- data/spec/dead_simple_cms/attribute/type/base_spec.rb +117 -0
- data/spec/dead_simple_cms/configuration_spec.rb +117 -0
- data/spec/dead_simple_cms/file_uploader/base_spec.rb +35 -0
- data/spec/dead_simple_cms/group/configuration_spec.rb +24 -0
- data/spec/dead_simple_cms/group/presenter_spec.rb +28 -0
- data/spec/dead_simple_cms/group_spec.rb +65 -0
- data/spec/dead_simple_cms/rails/action_view/form_builders/default_spec.rb +8 -0
- data/spec/dead_simple_cms/rails/action_view/form_builders/simple_form_spec.rb +8 -0
- data/spec/dead_simple_cms/rails/action_view/form_builders/simple_form_with_bootstrap_spec.rb +8 -0
- data/spec/dead_simple_cms/rails/action_view/fragment_sweeper_spec.rb +22 -0
- data/spec/dead_simple_cms/rails/action_view/presenter_spec.rb +54 -0
- data/spec/dead_simple_cms/section/builder_spec.rb +28 -0
- data/spec/dead_simple_cms/section_spec.rb +78 -0
- data/spec/dead_simple_cms/storage/base_spec.rb +59 -0
- data/spec/dead_simple_cms/storage/database_spec.rb +51 -0
- data/spec/dead_simple_cms/storage/memory_spec.rb +25 -0
- data/spec/dead_simple_cms/storage/rails_cache_spec.rb +33 -0
- data/spec/dead_simple_cms/storage/redis_spec.rb +33 -0
- data/spec/dead_simple_cms/util/identifier/dictionary/duplciate_item_spec.rb +10 -0
- data/spec/dead_simple_cms/util/identifier/dictionary/invalid_entry_class_spec.rb +10 -0
- data/spec/dead_simple_cms/util/identifier/dictionary_spec.rb +42 -0
- data/spec/dead_simple_cms/util/identifier_spec.rb +27 -0
- data/spec/setup.rb +26 -0
- data/spec/setup/banner_presenter.rb +19 -0
- data/spec/setup/rspec_template_builder.rb +104 -0
- data/spec/setup/shared.rb +57 -0
- data/spec/setup/test_file_uploader.rb +12 -0
- data/spec/spec_helper.rb +48 -0
- metadata +221 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionController
|
4
|
+
module Extensions
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def cms_cache_sweeper(options={})
|
10
|
+
cache_sweeper ActionView::FragmentSweeper, options
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Private: Update the values for the cms with the arguments from the params hash.
|
17
|
+
def update_sections_from_params
|
18
|
+
DeadSimpleCMS::Section.update_from_params(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionView
|
4
|
+
# Public: Class to handle expiring the fragment caches from the cms.
|
5
|
+
# ::ActionController::Caching::Sweeper inherits from ActiveRecord::Observer even though we aren't observing an
|
6
|
+
# ActiveRecord class. This still works though because the interface (update callback) is the same on the Section.
|
7
|
+
class FragmentSweeper < ::ActionController::Caching::Sweeper
|
8
|
+
|
9
|
+
observe DeadSimpleCMS::Section
|
10
|
+
|
11
|
+
# Make sure we expire any fragments that we are using on those pages.
|
12
|
+
def after_save(section)
|
13
|
+
section.fragments.each { |f| expire_fragment(f) }
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionView
|
4
|
+
module FormBuilders
|
5
|
+
# Public: Interface for buildering form builders to work with this library.
|
6
|
+
class Default < ::ActionView::Helpers::FormBuilder
|
7
|
+
|
8
|
+
include Interface
|
9
|
+
|
10
|
+
self.actions_options = {:class => "form-actions"}
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionView
|
4
|
+
module FormBuilders
|
5
|
+
# Public: Interface used for a form builder to work with dead simple cms.
|
6
|
+
module Interface
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
class_attribute :form_for_method
|
11
|
+
self.form_for_method = :form_for
|
12
|
+
|
13
|
+
class_inheritable_hash :form_for_options
|
14
|
+
self.form_for_options = {}
|
15
|
+
|
16
|
+
[:update, :actions, :preview, :attribute].each do |name|
|
17
|
+
option_name = "#{name}_options"
|
18
|
+
class_inheritable_hash option_name, :instance_writer => false
|
19
|
+
self.send("#{option_name}=", {})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def attribute(attribute)
|
26
|
+
label(attribute.identifier, attribute.label) +
|
27
|
+
case attribute.input_type
|
28
|
+
when :string then text_field(attribute.identifier)
|
29
|
+
when :text then text_area(attribute.identifier)
|
30
|
+
when :file then file_field(attribute.identifier)
|
31
|
+
when :select then select(attribute.identifier, attribute.collection)
|
32
|
+
when :radio then radio_buttons(attribute.identifier, attribute.collection)
|
33
|
+
else raise("Unknown type: #{attribute.identifier}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def actions(&block)
|
38
|
+
@template.content_tag(:div, actions_options, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update
|
42
|
+
submit("Update", update_options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def radio_buttons(identifier, collection)
|
46
|
+
collection.map { |v| radio_button(identifier, v) }.join.html_safe
|
47
|
+
end
|
48
|
+
|
49
|
+
def preview(section)
|
50
|
+
@template.link_to("Preview", section.path, preview_options) if section.path
|
51
|
+
end
|
52
|
+
|
53
|
+
def fields_for_group(section_or_group, options={})
|
54
|
+
section_or_group.groups.values.map do |group|
|
55
|
+
next if group.attributes.empty? && group.groups.empty?
|
56
|
+
fields_for(group.identifier, options) do |builder|
|
57
|
+
@template.content_tag(:fieldset) do
|
58
|
+
# Following fieldset/legend convention: https://github.com/twitter/bootstrap/issues/1214
|
59
|
+
legend = @template.content_tag(:legend, group.label) unless group.root? # don't show the group name if it's the root group.
|
60
|
+
attributes = group.attributes.values.map { |attribute| builder.attribute(attribute) }.join.html_safe
|
61
|
+
nested_groups = builder.fields_for_group(group, options)
|
62
|
+
[legend, attributes, nested_groups].join.html_safe
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end.join.html_safe
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionView
|
4
|
+
module FormBuilders
|
5
|
+
# Public: SimpleForm builder for use with the Dead Simple CMS
|
6
|
+
# The class names embed into this builder are for twitter bootstrap.
|
7
|
+
class SimpleForm < ::SimpleForm::FormBuilder
|
8
|
+
|
9
|
+
include Interface
|
10
|
+
|
11
|
+
# Public: Translation of the Attribute::Type::Base#input_type to :as option for SimpleForm.
|
12
|
+
AS_LOOKUP = Hash.new { |h, k| k }.update(:radio => :radio_buttons)
|
13
|
+
|
14
|
+
self.preview_options = {:target => "_blank"}
|
15
|
+
|
16
|
+
def self.form_for_method
|
17
|
+
:simple_form_for
|
18
|
+
end
|
19
|
+
|
20
|
+
def attribute(attribute)
|
21
|
+
as = AS_LOOKUP[attribute.input_type]
|
22
|
+
hint = attribute.hint.to_s.dup
|
23
|
+
hint << %{ <a href="#{attribute.value}" target="_blank">preview</a>} if attribute.is_a?(Attribute::Type::Image)
|
24
|
+
options = {:required => attribute.required, :hint => hint, :as => as, :label => attribute.label}
|
25
|
+
if attribute.is_a?(Attribute::Type::CollectionSupport) && (collection = attribute.collection)
|
26
|
+
options[:collection] = collection
|
27
|
+
options[:as] = as
|
28
|
+
options[:include_blank] = false if as==:select
|
29
|
+
end
|
30
|
+
|
31
|
+
input(attribute.identifier, attribute_options.update(options))
|
32
|
+
end
|
33
|
+
|
34
|
+
def update
|
35
|
+
button(:submit, "Update", update_options)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
module Rails
|
3
|
+
module ActionView
|
4
|
+
module FormBuilders
|
5
|
+
class SimpleFormWithBootstrap < SimpleForm
|
6
|
+
|
7
|
+
self.form_for_options = {:wrapper => :bootstrap, :html => { class: "form-horizontal well" }}
|
8
|
+
self.update_options = {:class => "btn-primary"}
|
9
|
+
self.actions_options = {:class => "form-actions btn-group"}
|
10
|
+
self.preview_options = {:class => "btn"}
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
module DeadSimpleCMS
|
3
|
+
module Rails
|
4
|
+
module ActionView
|
5
|
+
class Presenter < SimpleDelegator
|
6
|
+
|
7
|
+
class_attribute :form_builder
|
8
|
+
|
9
|
+
def initialize(template)
|
10
|
+
__setobj__(template)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Renders a group into the view.
|
14
|
+
def group(group, *args, &block)
|
15
|
+
group.render(__getobj__, *args, &block) # pass through the action_view template.
|
16
|
+
end
|
17
|
+
|
18
|
+
def sections
|
19
|
+
DeadSimpleCMS.sections.values.map { |section| section(section, options) }.join.html_safe
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Render the dead simple cms with tabs
|
23
|
+
# This is *coupled* with twitter bootstrap. If you are running it, this will work great, otherwise this will be sad.
|
24
|
+
# http://twitter.github.com/bootstrap/javascript.html#tabs
|
25
|
+
def with_bootstrap_tabs(options={})
|
26
|
+
js = javascript_include_tag(options.delete(:bootstrap_tab_js) || "bootstrap-2.0.3/bootstrap-tab.js")
|
27
|
+
js << javascript_tag(<<-JAVASCRIPT)
|
28
|
+
$(document).ready(function() {
|
29
|
+
var $tabs = $('#section-tabs a') ;
|
30
|
+
$tabs.click(function (e) { e.preventDefault(); $(this).tab('show'); });
|
31
|
+
$("#" + (window.location.hash.replace("#", "") || "#{DeadSimpleCMS.sections.identifiers[0]}") + "-tab").tab('show');
|
32
|
+
})
|
33
|
+
JAVASCRIPT
|
34
|
+
lis = DeadSimpleCMS.sections.values.map do |section|
|
35
|
+
content_tag(:li, link_to(section.label, "##{section.identifier.to_s.csserize}", :id => "#{section.identifier.to_s.csserize}-tab"))
|
36
|
+
end.join.html_safe
|
37
|
+
tabs = content_tag(:ul, lis, :class => "nav nav-tabs", :id => "section-tabs")
|
38
|
+
sections = content_tag(:div, :class => "tab-content") do
|
39
|
+
DeadSimpleCMS.sections.values.map do |section|
|
40
|
+
content_tag(:div, section(section), :class => "tab-pane", :id => "#{section.identifier.to_s.csserize}")
|
41
|
+
end.join.html_safe
|
42
|
+
end
|
43
|
+
js + tabs + sections
|
44
|
+
end
|
45
|
+
|
46
|
+
def section(section, options={})
|
47
|
+
content_tag(:h2, section.label) + form(section, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def form(section, options={})
|
51
|
+
options.reverse_merge!(:builder => form_builder, :url => {:action => :edit})
|
52
|
+
options.reverse_merge!(options[:builder].form_for_options)
|
53
|
+
|
54
|
+
(options[:html] ||= {}).update(:multipart => true) if section.attributes.values.any? { |a| a.input_type == :file }
|
55
|
+
|
56
|
+
send(options[:builder].form_for_method, section, {:as => section.identifier}.update(options)) do |form|
|
57
|
+
form.fields_for_group(section, options) + form.actions { form.update + form.preview(section) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
# Public: Represents a section in the website a user wants to be able to modify the data in an immediate-simple way.
|
3
|
+
class Section < Group
|
4
|
+
|
5
|
+
include Util::Identifier
|
6
|
+
|
7
|
+
include ActiveModel::Observing
|
8
|
+
|
9
|
+
extend ActiveModel::Callbacks
|
10
|
+
define_model_callbacks :save
|
11
|
+
|
12
|
+
self.dictionary_identifier_method = :section_identifier
|
13
|
+
|
14
|
+
# Public: a Symbol representing the default storage class
|
15
|
+
class_attribute :storage_class, :instance_writer => false
|
16
|
+
|
17
|
+
attr_reader :path, :root_group
|
18
|
+
|
19
|
+
before_save :upload_file_attributes
|
20
|
+
|
21
|
+
def initialize(identifier, options={}, &block)
|
22
|
+
super(identifier, options)
|
23
|
+
@path = options[:path]
|
24
|
+
@root_group = Group.root # The root group for the section.
|
25
|
+
@fragments = Array.wrap(options[:fragments])
|
26
|
+
add_group(@root_group)
|
27
|
+
|
28
|
+
build(&block) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: Update the sections with the params and return the updated sections.
|
32
|
+
def self.update_from_params(params)
|
33
|
+
params.map do |section_identifier, attributes|
|
34
|
+
DeadSimpleCMS.sections[section_identifier].tap do |section|
|
35
|
+
section.update_attributes(attributes) if section
|
36
|
+
end
|
37
|
+
end.compact
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_attribute(attribute)
|
41
|
+
super
|
42
|
+
attribute.section = self
|
43
|
+
end
|
44
|
+
|
45
|
+
def fragments
|
46
|
+
@fragments.map { |f| f.is_a?(Proc) ? f.call(self) : f }
|
47
|
+
end
|
48
|
+
|
49
|
+
def storage
|
50
|
+
@storage ||= storage_class.new(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Update the section values.
|
54
|
+
#
|
55
|
+
# Examples
|
56
|
+
#
|
57
|
+
# update_attributes("left_attributes" => {"header" => "Hi There", "paragraph" => "how's it going"}})
|
58
|
+
# update_attributes("left_header" => "Hi There", "left_paragraph" => "how's it going"})
|
59
|
+
#
|
60
|
+
# Both of these work assuming the user defined a group called "left" and has attributes in it called "header" and
|
61
|
+
# "paragraph". The "left_attributes=" method plays nicely with nested fields_for which the form builder dependes on.
|
62
|
+
def update_attributes(attributes)
|
63
|
+
super
|
64
|
+
save!
|
65
|
+
end
|
66
|
+
|
67
|
+
def save!
|
68
|
+
_run_save_callbacks { storage.write }
|
69
|
+
end
|
70
|
+
|
71
|
+
def build(&block)
|
72
|
+
Builder.new(self, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_param
|
76
|
+
identifier.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def cache_key
|
80
|
+
"cms/#{identifier}"
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Tried to make an observer class for the uploading, but had some trouble getting it working. - Aryk
|
86
|
+
def upload_file_attributes
|
87
|
+
attributes.values.each { |a| a.upload! if a.is_a?(Attribute::Type::File) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def attribute_accessor(attribute)
|
91
|
+
super
|
92
|
+
identifier = attribute.send(dictionary_identifier_method)
|
93
|
+
# If the attribute is in the root group, then create an alias from "root_foo" to simply "foo"
|
94
|
+
if attribute.root_group?
|
95
|
+
instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
96
|
+
alias #{attribute.identifier} #{identifier}
|
97
|
+
alias #{attribute.identifier}= #{identifier}=
|
98
|
+
RUBY
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module DeadSimpleCMS
|
2
|
+
class Section
|
3
|
+
# Public: A Builder class which provides a nice DSL to describe different sections of the site that a user wants to modify
|
4
|
+
# through a CMS.
|
5
|
+
class Builder
|
6
|
+
|
7
|
+
attr_accessor :group_hierarchy
|
8
|
+
attr_reader :section
|
9
|
+
|
10
|
+
def self.define_attribute_builder_method(klass)
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
+
def #{klass.builder_method_name}(identifier, options={})
|
13
|
+
group_hierarchy = self.group_hierarchy.presence || [section.root_group] # fallback on the root group
|
14
|
+
attribute = #{klass}.new(identifier, options.merge(:group_hierarchy => group_hierarchy, :section => section))
|
15
|
+
group_hierarchy.last.add_attribute(attribute)
|
16
|
+
section.add_attribute(attribute)
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(section, &block)
|
22
|
+
@section = section
|
23
|
+
@group_hierarchy = []
|
24
|
+
instance_eval(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def display(*args, &block)
|
28
|
+
(current_group || section).display(*args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def group(*args, &block)
|
32
|
+
options = args.extract_options!
|
33
|
+
attribute_options_by_identifier = options.delete(:attribute_options) || {}
|
34
|
+
|
35
|
+
identifier = args.first
|
36
|
+
# If no identifier provided, first key, value pair of the hash is the identifier => group_configuration.
|
37
|
+
identifier, group_configuration = options.shift unless identifier
|
38
|
+
unless group_configuration.is_a?(DeadSimpleCMS::Group::Configuration)
|
39
|
+
group_configuration = DeadSimpleCMS.group_configurations[group_configuration]
|
40
|
+
end
|
41
|
+
options.update(group_configuration.options) if group_configuration
|
42
|
+
group = Group.new(identifier, options)
|
43
|
+
|
44
|
+
nest_group(group) do
|
45
|
+
if group_configuration
|
46
|
+
display(group_configuration.presenter_class, &group_configuration.render_proc)
|
47
|
+
group_configuration.attribute_arguments.each do |attribute_identifier, (attribute_type, attribute_options)|
|
48
|
+
attribute_options = attribute_options.merge(attribute_options_by_identifier[attribute_identifier] || {})
|
49
|
+
send(attribute_type, attribute_identifier, attribute_options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
instance_eval(&block) if block_given?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def current_group
|
59
|
+
group_hierarchy.last
|
60
|
+
end
|
61
|
+
|
62
|
+
def nest_group(group)
|
63
|
+
tmp = group_hierarchy
|
64
|
+
(group_hierarchy.last || section).add_group(group) # chain it with the last group or section if its top-level.
|
65
|
+
self.group_hierarchy += [group]
|
66
|
+
yield
|
67
|
+
ensure
|
68
|
+
self.group_hierarchy = tmp
|
69
|
+
end
|
70
|
+
|
71
|
+
end # Builder
|
72
|
+
end
|
73
|
+
end
|