cockpit 0.0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +175 -0
- data/Rakefile +78 -0
- data/init.rb +1 -0
- data/lib/cockpit.rb +22 -0
- data/lib/cockpit/cockpit.rb +94 -0
- data/lib/cockpit/configuration.rb +218 -0
- data/lib/cockpit/definition.rb +55 -0
- data/lib/cockpit/extensions.rb +25 -0
- data/lib/cockpit/helper.rb +44 -0
- data/lib/cockpit/store.rb +103 -0
- data/lib/cockpit/tree_hash.rb +113 -0
- data/rails/init.rb +1 -0
- data/test/lib/database.rb +22 -0
- data/test/lib/user.rb +8 -0
- data/test/test_helper.rb +90 -0
- data/test/test_settings.rb +228 -0
- data/test/test_settings_in_database.rb +153 -0
- data/test/test_settings_on_model.rb +68 -0
- metadata +86 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module Cockpit
|
2
|
+
# Represents the definition of a preference for a particular model
|
3
|
+
class Definition
|
4
|
+
# The data type for the content stored in this preference type
|
5
|
+
attr_reader :type
|
6
|
+
|
7
|
+
def initialize(name, *args) #:nodoc:
|
8
|
+
options = args.extract_options!
|
9
|
+
|
10
|
+
@type = args.first ? args.first.to_sym : :boolean
|
11
|
+
|
12
|
+
# Create a column that will be responsible for typecasting
|
13
|
+
@column = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default], @type == :any ? nil : @type.to_s)
|
14
|
+
|
15
|
+
@group_defaults = (options[:group_defaults] || {}).inject({}) do |defaults, (group, default)|
|
16
|
+
defaults[group.is_a?(Symbol) ? group.to_s : group] = type_cast(default)
|
17
|
+
defaults
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The name of the preference
|
22
|
+
def name
|
23
|
+
@column.name
|
24
|
+
end
|
25
|
+
|
26
|
+
# The default value to use for the preference in case none have been
|
27
|
+
# previously defined
|
28
|
+
def default_value(group = nil)
|
29
|
+
@group_defaults.include?(group) ? @group_defaults[group] : @column.default
|
30
|
+
end
|
31
|
+
|
32
|
+
# Determines whether column backing this preference stores numberic values
|
33
|
+
def number?
|
34
|
+
@column.number?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Typecasts the value based on the type of preference that was defined.
|
38
|
+
# This uses ActiveRecord's typecast functionality so the same rules for
|
39
|
+
# typecasting a model's columns apply here.
|
40
|
+
def type_cast(value)
|
41
|
+
@type == :any ? value : @column.type_cast(value)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Typecasts the value to true/false depending on the type of preference
|
45
|
+
def query(value)
|
46
|
+
if !(value = type_cast(value))
|
47
|
+
false
|
48
|
+
elsif number?
|
49
|
+
!value.zero?
|
50
|
+
else
|
51
|
+
!value.blank?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Hash
|
2
|
+
def recursively_symbolize_keys!
|
3
|
+
self.symbolize_keys!
|
4
|
+
self.values.each do |v|
|
5
|
+
if v.is_a? Hash
|
6
|
+
v.recursively_symbolize_keys!
|
7
|
+
elsif v.is_a? Array
|
8
|
+
v.recursively_symbolize_keys!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Array
|
16
|
+
def recursively_symbolize_keys!
|
17
|
+
self.each do |item|
|
18
|
+
if item.is_a? Hash
|
19
|
+
item.recursively_symbolize_keys!
|
20
|
+
elsif item.is_a? Array
|
21
|
+
item.recursively_symbolize_keys!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Cockpit::Helper
|
2
|
+
|
3
|
+
# always returns either an array or a string
|
4
|
+
def c(*args)
|
5
|
+
options = args.extract_options!
|
6
|
+
result = args.collect {|i| Settings.get(i).value }
|
7
|
+
result = result.pop if result.length == 1
|
8
|
+
result.blank? ? nil : result.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
def setting_tag(tag)
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def settings_tag(key, &block)
|
16
|
+
Settings(key).each_setting do |key, attributes, value|
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def setting_value(value)
|
22
|
+
result = case value
|
23
|
+
when Proc
|
24
|
+
value.call
|
25
|
+
when Cockpit::TreeHash
|
26
|
+
value
|
27
|
+
else
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def setting_options(attributes)
|
33
|
+
return {} unless (attributes.is_a?(Hash) && attributes[:options])
|
34
|
+
options = case attributes[:options]
|
35
|
+
when Proc
|
36
|
+
attributes[:options].call
|
37
|
+
else
|
38
|
+
attributes[:options]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
ActionView::Base.send(:include, Cockpit::Helper) if defined?(ActionView)
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Cockpit
|
2
|
+
module Store
|
3
|
+
class Base
|
4
|
+
attr_accessor :configurable
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
options = args.extract_options!
|
8
|
+
options.each do |k, v|
|
9
|
+
self.send("#{k.to_s}=", v) if self.respond_to?("#{k.to_s}=")
|
10
|
+
end
|
11
|
+
@configuration = args.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def configuration
|
15
|
+
@configuration
|
16
|
+
end
|
17
|
+
|
18
|
+
def tree=(value)
|
19
|
+
@tree = value if value.is_a?(TreeHash)
|
20
|
+
@tree
|
21
|
+
end
|
22
|
+
|
23
|
+
def tree
|
24
|
+
@tree ||= TreeHash.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear(options = {})
|
28
|
+
tree.each do |k, v|
|
29
|
+
tree.delete(k) unless (options[:except] && options[:except].include?(k.to_sym))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(path)
|
34
|
+
tree.get(path)
|
35
|
+
end
|
36
|
+
alias_method :[], :get
|
37
|
+
|
38
|
+
def get!(path)
|
39
|
+
result = get(path)
|
40
|
+
raise "'#{path.to_s}' was not in Config" if result.blank?
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def set(value)
|
45
|
+
return unless value.is_a?(Hash)
|
46
|
+
value.each { |k,v| set_one(k, v) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_one(key, value)
|
50
|
+
tree.set(key, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def set!(value)
|
54
|
+
result = set(value)
|
55
|
+
raise "'#{path.to_s}' was not set in Config" if result.blank?
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def []=(key, value)
|
60
|
+
set_one(key => value)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Memory < Base
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class Database < Base
|
69
|
+
|
70
|
+
def clear(options = {})
|
71
|
+
configuration.setting_class.all.collect(&:destroy) if options[:hard] == true
|
72
|
+
super(options)
|
73
|
+
end
|
74
|
+
|
75
|
+
def find_or_create(key)
|
76
|
+
result = configuration.setting_class.find(key.to_s) rescue nil
|
77
|
+
result ||= configuration.setting_class.create(:key => key.to_s)
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_one_with_database(key, value)
|
81
|
+
setting = find_or_create(key)
|
82
|
+
cast_as = (setting.cast_as || Cockpit.get_type(value)).to_s
|
83
|
+
attributes = {:value => value, :cast_as => cast_as}
|
84
|
+
attributes[:configurable] = self.configurable if self.configurable
|
85
|
+
setting.update_attributes(attributes)
|
86
|
+
set_one_without_database(key, Cockpit.type_cast(value, cast_as))
|
87
|
+
end
|
88
|
+
alias_method_chain :set_one, :database
|
89
|
+
|
90
|
+
def get(key)
|
91
|
+
result = super(key)
|
92
|
+
if result.blank? && setting = find_or_create(key)
|
93
|
+
set(key => setting.value)
|
94
|
+
result = super(key)
|
95
|
+
else
|
96
|
+
|
97
|
+
end
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# http://www.daniel-azuma.com/blog/view/z3bqa0t01uugg1/implementing_dsl_blocks
|
2
|
+
module Cockpit
|
3
|
+
class TreeHash < Hash
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
block = Proc.new {|h,k| h[k] = TreeHash.new(&block)}
|
7
|
+
super &block
|
8
|
+
end
|
9
|
+
|
10
|
+
def value_type
|
11
|
+
has_key?(:type) ? self[:type] : :string
|
12
|
+
end
|
13
|
+
|
14
|
+
def each_setting(&block)
|
15
|
+
dup.each do |k, v|
|
16
|
+
atrib = Hash.new
|
17
|
+
if v.has_key?(:value)
|
18
|
+
value = clone(v[:value])
|
19
|
+
atrib = clone(v)
|
20
|
+
else
|
21
|
+
v.each do |k, sub_v|
|
22
|
+
atrib[k] = clone(v[k]) unless sub_v.is_a?(TreeHash)
|
23
|
+
end
|
24
|
+
value = clone(v)
|
25
|
+
end
|
26
|
+
yield(k.to_s, atrib, value) if block_given?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def dup
|
31
|
+
result = super
|
32
|
+
result.each do |k,v|
|
33
|
+
result[k] = v.dup if v.is_a?(TreeHash)
|
34
|
+
end
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
def children
|
39
|
+
hash = {}
|
40
|
+
each do |k, v|
|
41
|
+
hash[k] = v if v.is_a?(Hash)
|
42
|
+
end
|
43
|
+
hash
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(key)
|
47
|
+
traverse(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_attribute(key, attribute)
|
51
|
+
get(key)[attribute.to_sym]
|
52
|
+
end
|
53
|
+
|
54
|
+
def set(key, value)
|
55
|
+
traverse(key, value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_attribute(key, value)
|
59
|
+
traverse(key, value, false)
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_attributes(hash)
|
63
|
+
hash.each { |k, v| set_attribute(k, v) }
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_hash
|
68
|
+
hash = {}
|
69
|
+
self.each do |k, v|
|
70
|
+
hash[k] = v.is_a?(TreeHash) ? v.to_hash : v
|
71
|
+
end
|
72
|
+
hash
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
def traverse(path, value = nil, as_node = true)
|
77
|
+
path = path.to_s.split('.')
|
78
|
+
child = path.pop.to_sym
|
79
|
+
parent = path.inject(self) { |h,k| h[k.to_sym] }
|
80
|
+
unless value.nil?
|
81
|
+
if as_node
|
82
|
+
if parent[child].has_key?(:type)
|
83
|
+
parent[child][:value] = Cockpit.type_cast(value, parent[child][:type])
|
84
|
+
else
|
85
|
+
parent[child][:value] = value
|
86
|
+
parent[child][:type] = Cockpit.get_type(value)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
parent[child] = value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
parent[child]
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(meth, *args, &block)
|
96
|
+
options = args.extract_options!
|
97
|
+
meth = meth.to_s.gsub("=", "").to_sym
|
98
|
+
if args.empty?
|
99
|
+
return self[meth] if self.has_key?(meth)
|
100
|
+
found = get(meth).set_attributes(options)
|
101
|
+
found = found.instance_eval(&block) if block_given?
|
102
|
+
found
|
103
|
+
else
|
104
|
+
get(meth).set_attributes({:type => Cockpit.get_type(args.first)}.merge(options).merge(:value => args.first))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
def clone(object)
|
110
|
+
(object.respond_to?(:dup) ? object.dup : object) rescue object
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'cockpit'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
begin
|
2
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
3
|
+
rescue ArgumentError
|
4
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
5
|
+
end
|
6
|
+
|
7
|
+
ActiveRecord::Base.configurations = true
|
8
|
+
|
9
|
+
ActiveRecord::Schema.define(:version => 1) do
|
10
|
+
|
11
|
+
create_table :settings, :force => true do |t|
|
12
|
+
t.string :key
|
13
|
+
t.string :value
|
14
|
+
t.string :cast_as
|
15
|
+
t.string :configurable_type
|
16
|
+
t.integer :configurable_id
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table "users", :force => true do |t|
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/test/lib/user.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "ruby-debug"
|
3
|
+
gem 'test-unit'
|
4
|
+
require "test/unit"
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/test_case'
|
7
|
+
require 'active_record'
|
8
|
+
require 'active_record/fixtures'
|
9
|
+
require 'shoulda'
|
10
|
+
require 'shoulda/active_record'
|
11
|
+
|
12
|
+
require File.dirname(__FILE__) + '/lib/database'
|
13
|
+
|
14
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '/../lib/cockpit'))
|
15
|
+
|
16
|
+
require File.dirname(__FILE__) + '/lib/user'
|
17
|
+
|
18
|
+
ActiveRecord::Base.class_eval do
|
19
|
+
def self.detonate
|
20
|
+
all.map(&:destroy)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveSupport::TestCase.class_eval do
|
25
|
+
|
26
|
+
def load_settings
|
27
|
+
Settings do
|
28
|
+
asset :title => "Asset (and related) Settings" do
|
29
|
+
thumb do
|
30
|
+
width 100, :tip => "Thumb's width"
|
31
|
+
height 100, :tip => "Thumb's height"
|
32
|
+
end
|
33
|
+
medium do
|
34
|
+
width 600, :tip => "Thumb's width"
|
35
|
+
height 250, :tip => "Thumb's height"
|
36
|
+
end
|
37
|
+
large do
|
38
|
+
width 600, :tip => "Large's width"
|
39
|
+
height 295, :tip => "Large's height"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
authentication :title => "Authentication Settings" do
|
43
|
+
use_open_id true
|
44
|
+
use_oauth true
|
45
|
+
end
|
46
|
+
front_page do
|
47
|
+
slideshow_tag "slideshow"
|
48
|
+
slideshow_effect "fade"
|
49
|
+
end
|
50
|
+
page do
|
51
|
+
per_page 10
|
52
|
+
feed_per_page 10
|
53
|
+
end
|
54
|
+
people do
|
55
|
+
show_avatars true
|
56
|
+
default_avatar "/images/missing-person.png"
|
57
|
+
end
|
58
|
+
site do
|
59
|
+
title "Martini"
|
60
|
+
tagline "Developer Friendly, Client Ready Blog with Rails 3"
|
61
|
+
keywords "Rails 3, Heroku, JQuery, HTML 5, Blog Engine, CSS3"
|
62
|
+
copyright "© 2010 Viatropos. All rights reserved."
|
63
|
+
timezones :value => lambda { TimeZone.first }, :options => lambda { TimeZone.all }
|
64
|
+
date_format "%m %d, %Y"
|
65
|
+
time_format "%H"
|
66
|
+
week_starts_on "Monday", :options => ["Monday", "Sunday", "Friday"]
|
67
|
+
language "en-US", :options => ["en-US", "de"]
|
68
|
+
touch_enabled true
|
69
|
+
touch_as_subdomain false
|
70
|
+
google_analytics ""
|
71
|
+
teasers :title => "Teasers" do
|
72
|
+
disable false
|
73
|
+
left 1, :title => "Left Teaser"
|
74
|
+
right 2
|
75
|
+
center 3
|
76
|
+
end
|
77
|
+
main_quote 1
|
78
|
+
end
|
79
|
+
social do
|
80
|
+
facebook "http://facebook.com/viatropos"
|
81
|
+
twitter "http://twitter.com/viatropos"
|
82
|
+
email "lancejpollard@gmail.com"
|
83
|
+
end
|
84
|
+
s3 do
|
85
|
+
key "my_key"
|
86
|
+
secret "my_secret"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|