dead_simple_cms 0.9.0

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