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,45 @@
1
+ module DeadSimpleCMS
2
+ module Attribute
3
+ class Collection
4
+ include Util::Identifier
5
+
6
+ attr_reader :attributes
7
+
8
+ delegate :[], :[]=, :to => :attributes
9
+
10
+ # The method used to construct the identifier (key) by which the attribute (value) will be stored with.
11
+ class_attribute :dictionary_identifier_method
12
+ self.dictionary_identifier_method = :identifier
13
+
14
+ def initialize(identifier, options={})
15
+ @attributes = Attribute::Type::Base.new_dictionary(:identifier_method => dictionary_identifier_method)
16
+ super
17
+ end
18
+
19
+ # Play nicely with Rails fields_for.
20
+ def persisted?
21
+ false
22
+ end
23
+
24
+ def update_attributes(attributes)
25
+ attributes.each { |k, v| send("#{k}=", v) }
26
+ end
27
+
28
+ def add_attribute(attribute)
29
+ attributes.add(attribute)
30
+ attribute_accessor(attribute)
31
+ end
32
+
33
+ private
34
+
35
+ def attribute_accessor(attribute)
36
+ identifier = attribute.send(dictionary_identifier_method)
37
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
38
+ def #{identifier} ; attributes[#{identifier.inspect}].value ; end
39
+ def #{identifier}=(v) ; attributes[#{identifier.inspect}].value = v ; end
40
+ RUBY
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,104 @@
1
+ module DeadSimpleCMS
2
+ module Attribute
3
+ module Type
4
+ class String < Base
5
+ self.default_input_type = :string
6
+ include CollectionSupport
7
+ end
8
+ class Text < String
9
+ self.default_input_type = :text
10
+ end
11
+ class Symbol < Base
12
+ self.default_input_type = :string
13
+ def convert_value(value)
14
+ value.to_s.to_sym if value.present?
15
+ end
16
+ private :convert_value
17
+ end
18
+ class Boolean < Base
19
+ self.default_input_type = :radio
20
+
21
+ include CollectionSupport
22
+
23
+ def initialize(identifier, options={})
24
+ options.update(:collection => [true, false], :default => false)
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def convert_value(value)
31
+ value.is_a?(::String) ? ["true", "1"].include?(value.downcase) : !!value
32
+ end
33
+
34
+ end
35
+ class Numeric < Base
36
+ self.default_input_type = :string
37
+ include CollectionSupport
38
+ end
39
+ class Integer < Numeric
40
+ def convert_value(value)
41
+ value && value.to_i
42
+ end
43
+ end
44
+ # Public: File attributes are stored at some publicly accessible url.
45
+ class File < Base
46
+ self.default_input_type = :file
47
+
48
+ attr_accessor :data, :file_ext
49
+
50
+ class_attribute :uploader_class
51
+
52
+ alias :url :value
53
+
54
+ def initialize(identifier, options={})
55
+ @data = options[:data]
56
+ @file_ext = options[:file_ext] || "dat"
57
+ super
58
+ end
59
+
60
+ # Public: Takes the current #value, detects if its an object to upload and replaces the #value with the url
61
+ # to be stored in the CMS.
62
+ def upload!
63
+ raise NotImplementedError, "Please define an uploader class (see DeadSimpleCMS::FileUploader::Base)" unless uploader_class
64
+ return unless data
65
+
66
+ s3_uploader = uploader_class.new(self)
67
+ s3_uploader.upload!
68
+ self.value = s3_uploader.url
69
+ end
70
+
71
+ private
72
+
73
+ def convert_value(value)
74
+ case value
75
+ when ::String, NilClass
76
+ value
77
+ when ActionDispatch::Http::UploadedFile
78
+ @file_ext = value.original_filename[/\.(.+)$/, 1]
79
+ value.rewind # make sure it's rewound
80
+ self.data = value.read
81
+ @value # just return the same stored @value so it doesn't change for now
82
+ else
83
+ raise("Don't know how to convert value: #{value.inspect}")
84
+ end
85
+ end
86
+
87
+ end
88
+ class Image < File
89
+
90
+ attr_reader :width, :height
91
+
92
+ def initialize(identifier, options={})
93
+ @width, @height = options.values_at(:width, :height)
94
+ super
95
+ end
96
+
97
+ def hint
98
+ super || "Image should be #{width} x #{height}."
99
+ end
100
+
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,79 @@
1
+ module DeadSimpleCMS
2
+ module Attribute
3
+ module Type
4
+ class Base
5
+
6
+ include Util::Identifier
7
+
8
+ class << self
9
+ # Public: the method name used to identify this type in the Builder
10
+ attr_writer :builder_method_name
11
+
12
+ # If not provided on the subclass infer it from the class name. Because of how class_attribute works, this
13
+ # method will be overwritten when someone explicitly calls .builder_method_name= on a subclass.
14
+ def builder_method_name
15
+ @builder_method_name ||= name.demodulize.underscore
16
+ end
17
+
18
+ end
19
+
20
+ # Public: a Symbol representing the default input type required for forms
21
+ class_attribute :default_input_type, :instance_writer => false
22
+
23
+ VALID_INPUT_TYPES = [:string, :text, :select, :file, :radio].freeze
24
+
25
+
26
+ attr_reader :hint, :input_type, :group_hierarchy, :required
27
+ attr_accessor :section
28
+
29
+ def initialize(identifier, options={})
30
+ options.reverse_merge!(:group_hierarchy => [], :input_type => default_input_type, :required => false)
31
+ @hint, @default, @input_type, @group_hierarchy, @section, @required =
32
+ options.values_at(:hint, :default, :input_type, :group_hierarchy, :section, :required)
33
+ raise("Invalid input type: #{input_type.inspect}. Should be one of #{VALID_INPUT_TYPES}.") unless VALID_INPUT_TYPES.include?(input_type)
34
+ super
35
+ end
36
+
37
+ def root_group?
38
+ group_hierarchy.last.try(:root?)
39
+ end
40
+
41
+ # Public: The identifier on the section level. It must be unique amongst the groups.
42
+ def section_identifier
43
+ (group_hierarchy + [self]).map(&:identifier).join("_").to_sym
44
+ end
45
+
46
+ def default
47
+ @default.is_a?(Proc) ? @default.call : @default
48
+ end
49
+
50
+ def value=(value)
51
+ @value = convert_value(value)
52
+ end
53
+
54
+ # Public: Returns the non-blank value from the storage or the default.
55
+ def value
56
+ return @value if instance_variable_defined?(:@value) # If the value was set to nil, we should return that value.
57
+ attributes = attributes_from_storage
58
+ @value = attributes.key?(section_identifier) ? attributes[section_identifier] : default
59
+ end
60
+
61
+ def inspect
62
+ ivars = [:identifier, :hint, :default, :required, :input_type].map { |m| ivar = "@#{m}" ; "#{ivar}=#{instance_variable_get(ivar).inspect}" }
63
+ "#<#{self.class} #{ivars.join(", ")}"
64
+ end
65
+
66
+ private
67
+
68
+ def attributes_from_storage
69
+ section.storage.read
70
+ end
71
+
72
+ def convert_value(value)
73
+ value.presence
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,26 @@
1
+ module DeadSimpleCMS
2
+ module Attribute
3
+ module Type
4
+ module CollectionSupport
5
+
6
+ VALID_COLLECTION_INPUT_TYPES = [:select, :radio].freeze
7
+
8
+ DEFAULT_COLLECTION_INPUT_TYPE = :select
9
+
10
+ def initialize(identifier, options={})
11
+ if @collection = options[:collection]
12
+ # Pick either the options[:input_type], default_input_type, or the :select. Whichever matches the valid types.
13
+ options[:input_type] = ([options[:input_type], default_input_type, DEFAULT_COLLECTION_INPUT_TYPE] & VALID_COLLECTION_INPUT_TYPES).first
14
+ end
15
+ super
16
+ end
17
+
18
+ # Public: For performance and loading reasons, we allow the ability to pass through a collection as a lambda.
19
+ def collection
20
+ @collection.is_a?(Proc) ? @collection.call : @collection
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,50 @@
1
+ module DeadSimpleCMS
2
+ class Configuration
3
+
4
+ def section(identifier, options={}, &block)
5
+ section = DeadSimpleCMS::Section.new(identifier, options, &block)
6
+ DeadSimpleCMS.sections.add(section)
7
+ # Convenience method, allows one to access DeadSimpleCMS.sections.<section_name>
8
+ DeadSimpleCMS.sections.instance_eval %{def #{section.identifier} ; self[#{section.identifier.inspect}] ; end}
9
+ end
10
+
11
+ def group_configuration(identifier, options={}, &block)
12
+ DeadSimpleCMS.group_configurations.add(Group::Configuration.new(identifier, options, &block))
13
+ end
14
+
15
+ def register_attribute_classes(*classes)
16
+ classes.each do |klass|
17
+ DeadSimpleCMS::Section::Builder.define_attribute_builder_method(klass)
18
+ Group::Configuration.define_attribute_builder_method(klass)
19
+ end
20
+ end
21
+
22
+ def storage_class(klass=nil, options={})
23
+ if klass
24
+ klass = "DeadSimpleCMS::Storage::#{klass.to_s.classify}".constantize if klass.is_a?(Symbol)
25
+ options.each { |k, v| klass.send("#{k}=", v) }
26
+ Section.storage_class = klass
27
+ end
28
+ Section.storage_class
29
+ end
30
+
31
+ def storage_serializer_class(klass=nil)
32
+ Storage::Base.serializer_class = klass if klass
33
+ Storage::Base.serializer_class
34
+ end
35
+
36
+ def default_form_builder(klass=nil)
37
+ if klass
38
+ klass = "DeadSimpleCMS::Rails::ActionView::FormBuilders::#{klass.to_s.classify}".constantize if klass.is_a?(Symbol)
39
+ DeadSimpleCMS::Rails::ActionView::Presenter.form_builder = klass
40
+ end
41
+ DeadSimpleCMS::Rails::ActionView::Presenter.form_builder
42
+ end
43
+
44
+ def file_uploader_class(klass=nil)
45
+ Attribute::Type::File.uploader_class = klass if klass
46
+ Attribute::Type::File.uploader_class
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ module DeadSimpleCMS
2
+ module FileUploader
3
+ # Base file uploader class. Inherit from this to build your own uploaders for files.
4
+ class Base
5
+
6
+ attr_reader :file_attribute
7
+ delegate :file_ext, :data, :to => :file_attribute
8
+
9
+ def initialize(file_attribute)
10
+ @file_attribute = file_attribute
11
+ end
12
+
13
+ def upload!
14
+ raise NotImplementedError, "Please overwrite this with your own upload functionality."
15
+ end
16
+
17
+ def url
18
+ raise NotImplementedError, "Please overwrite this with your own url constructor."
19
+ end
20
+
21
+ # Public: We need to have all these nesting to protect against corner-case collisons.
22
+ def path(namespace="dead_simple_cms")
23
+ # dead_simple_cms/<section>/<group>/attribute.jpg
24
+ @path ||= [namespace, *[file_attribute.section, *file_attribute.group_hierarchy, file_attribute].map(&:identifier)].compact * "/" + "." + file_ext
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,58 @@
1
+ module DeadSimpleCMS
2
+ # Public: A Group is essentially an Attribute::Collection with the ability to have it's own groups on it with infinite
3
+ # nesting of groups.
4
+ class Group < Attribute::Collection
5
+
6
+ ROOT_IDENTIFIER = :root
7
+
8
+ attr_reader :groups, :presenter_class, :render_proc
9
+
10
+ def initialize(identifier, options={})
11
+ @groups = DeadSimpleCMS::Group.new_dictionary
12
+ super
13
+ end
14
+
15
+ def self.root
16
+ new(ROOT_IDENTIFIER)
17
+ end
18
+
19
+ # Public: Set different mechanisms for rendering this group.
20
+ def display(presenter_class=nil, &block)
21
+ @presenter_class = presenter_class
22
+ @render_proc = block
23
+ end
24
+
25
+ # Public: If a presenter class was specified, returns an instance of the presenter.
26
+ def presenter(view_context, *args)
27
+ @presenter_class.new(view_context, *args) if @presenter_class
28
+ end
29
+
30
+ # Public: Render the group using the passed in proc in the scope of the template.
31
+ def render(view_context, *args)
32
+ if @render_proc
33
+ view_context.instance_exec(self, *args, &@render_proc)
34
+ elsif presenter = presenter(view_context, self, *args)
35
+ presenter.render
36
+ end
37
+ end
38
+
39
+ def root?
40
+ identifier == ROOT_IDENTIFIER
41
+ end
42
+
43
+ def add_group(group)
44
+ groups.add(group)
45
+ group_accessor(group)
46
+ end
47
+
48
+ private
49
+
50
+ # The <group>_attributes method plays nice with form builder's fields_for.
51
+ def group_accessor(group)
52
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
53
+ def #{group.identifier}_attributes=(v) ; groups[#{group.identifier.inspect}].update_attributes(v) ; end
54
+ def #{group.identifier} ; groups[#{group.identifier.inspect}] ; end
55
+ RUBY
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ module DeadSimpleCMS
2
+ class Group
3
+ class Configuration
4
+
5
+ include Util::Identifier
6
+
7
+ attr_reader :options, :attribute_arguments, :presenter_class, :render_proc
8
+
9
+ def initialize(identifier, options={}, &block)
10
+ super(identifier, options)
11
+ @options = options
12
+ @attribute_arguments = {}
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def display(presenter_class=nil, &block)
17
+ @presenter_class = presenter_class if presenter_class
18
+ @render_proc = block if block_given?
19
+ end
20
+
21
+ def self.define_attribute_builder_method(klass)
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
+ def #{klass.builder_method_name}(identifier, options={})
24
+ attribute_arguments[identifier] = [#{klass.builder_method_name.inspect}, options]
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ require 'delegate'
2
+ module DeadSimpleCMS
3
+ class Group
4
+ # Public: Presenter class used for rendering groups.
5
+ #
6
+ # Within the context of this group, you can call methods as if you were in the view_context.
7
+ class Presenter < SimpleDelegator
8
+
9
+ attr_reader :group
10
+
11
+ def initialize(view_context, group, *args)
12
+ @group = group
13
+ initialize_extra_arguments(*args)
14
+ super(view_context)
15
+ end
16
+
17
+ # Private: Initialize extra arguments for the presenter.
18
+ def initialize_extra_arguments(*args)
19
+ end
20
+
21
+ end
22
+ end
23
+ end