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.
Files changed (66) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +266 -0
  6. data/Rakefile +17 -0
  7. data/dead_simple_cms.gemspec +47 -0
  8. data/lib/dead_simple_cms.rb +132 -0
  9. data/lib/dead_simple_cms/attribute/collection.rb +45 -0
  10. data/lib/dead_simple_cms/attribute/type/all.rb +104 -0
  11. data/lib/dead_simple_cms/attribute/type/base.rb +79 -0
  12. data/lib/dead_simple_cms/attribute/type/collection_support.rb +26 -0
  13. data/lib/dead_simple_cms/configuration.rb +50 -0
  14. data/lib/dead_simple_cms/file_uploader/base.rb +29 -0
  15. data/lib/dead_simple_cms/group.rb +58 -0
  16. data/lib/dead_simple_cms/group/configuration.rb +32 -0
  17. data/lib/dead_simple_cms/group/presenter.rb +23 -0
  18. data/lib/dead_simple_cms/rails/action_controller/extensions.rb +24 -0
  19. data/lib/dead_simple_cms/rails/action_controller/fragment_sweeper.rb +19 -0
  20. data/lib/dead_simple_cms/rails/action_view/extensions.rb +14 -0
  21. data/lib/dead_simple_cms/rails/action_view/form_builders/default.rb +16 -0
  22. data/lib/dead_simple_cms/rails/action_view/form_builders/interface.rb +72 -0
  23. data/lib/dead_simple_cms/rails/action_view/form_builders/simple_form.rb +43 -0
  24. data/lib/dead_simple_cms/rails/action_view/form_builders/simple_form_with_bootstrap.rb +19 -0
  25. data/lib/dead_simple_cms/rails/action_view/presenter.rb +63 -0
  26. data/lib/dead_simple_cms/section.rb +103 -0
  27. data/lib/dead_simple_cms/section/builder.rb +73 -0
  28. data/lib/dead_simple_cms/storage/base.rb +49 -0
  29. data/lib/dead_simple_cms/storage/database.rb +33 -0
  30. data/lib/dead_simple_cms/storage/memory.rb +20 -0
  31. data/lib/dead_simple_cms/storage/rails_cache.rb +19 -0
  32. data/lib/dead_simple_cms/storage/redis.rb +23 -0
  33. data/lib/dead_simple_cms/util/identifier.rb +40 -0
  34. data/lib/dead_simple_cms/util/identifier/dictionary.rb +65 -0
  35. data/lib/dead_simple_cms/version.rb +3 -0
  36. data/spec/dead_simple_cms/attribute/collection_spec.rb +46 -0
  37. data/spec/dead_simple_cms/attribute/type/all_spec.rb +252 -0
  38. data/spec/dead_simple_cms/attribute/type/base_spec.rb +117 -0
  39. data/spec/dead_simple_cms/configuration_spec.rb +117 -0
  40. data/spec/dead_simple_cms/file_uploader/base_spec.rb +35 -0
  41. data/spec/dead_simple_cms/group/configuration_spec.rb +24 -0
  42. data/spec/dead_simple_cms/group/presenter_spec.rb +28 -0
  43. data/spec/dead_simple_cms/group_spec.rb +65 -0
  44. data/spec/dead_simple_cms/rails/action_view/form_builders/default_spec.rb +8 -0
  45. data/spec/dead_simple_cms/rails/action_view/form_builders/simple_form_spec.rb +8 -0
  46. data/spec/dead_simple_cms/rails/action_view/form_builders/simple_form_with_bootstrap_spec.rb +8 -0
  47. data/spec/dead_simple_cms/rails/action_view/fragment_sweeper_spec.rb +22 -0
  48. data/spec/dead_simple_cms/rails/action_view/presenter_spec.rb +54 -0
  49. data/spec/dead_simple_cms/section/builder_spec.rb +28 -0
  50. data/spec/dead_simple_cms/section_spec.rb +78 -0
  51. data/spec/dead_simple_cms/storage/base_spec.rb +59 -0
  52. data/spec/dead_simple_cms/storage/database_spec.rb +51 -0
  53. data/spec/dead_simple_cms/storage/memory_spec.rb +25 -0
  54. data/spec/dead_simple_cms/storage/rails_cache_spec.rb +33 -0
  55. data/spec/dead_simple_cms/storage/redis_spec.rb +33 -0
  56. data/spec/dead_simple_cms/util/identifier/dictionary/duplciate_item_spec.rb +10 -0
  57. data/spec/dead_simple_cms/util/identifier/dictionary/invalid_entry_class_spec.rb +10 -0
  58. data/spec/dead_simple_cms/util/identifier/dictionary_spec.rb +42 -0
  59. data/spec/dead_simple_cms/util/identifier_spec.rb +27 -0
  60. data/spec/setup.rb +26 -0
  61. data/spec/setup/banner_presenter.rb +19 -0
  62. data/spec/setup/rspec_template_builder.rb +104 -0
  63. data/spec/setup/shared.rb +57 -0
  64. data/spec/setup/test_file_uploader.rb +12 -0
  65. data/spec/spec_helper.rb +48 -0
  66. 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,14 @@
1
+ module DeadSimpleCMS
2
+ module Rails
3
+ module ActionView
4
+ module Extensions
5
+
6
+ def dead_simple_cms
7
+ @dead_simple_cms ||= Presenter.new(self)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -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