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