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,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
|