bowtie 0.3.2 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +68 -0
- data/bowtie.gemspec +33 -15
- data/lib/bowtie.rb +15 -7
- data/lib/bowtie/adapters/datamapper.rb +126 -0
- data/lib/bowtie/adapters/mongomapper.rb +122 -0
- data/lib/bowtie/admin.rb +24 -26
- data/lib/bowtie/core_extensions.rb +24 -28
- data/lib/bowtie/helpers.rb +23 -3
- data/lib/bowtie/public/css/bowtie.css +50 -9
- data/lib/bowtie/public/js/bowtie.js +1 -1
- data/lib/bowtie/views/errors.erb +1 -1
- data/lib/bowtie/views/field.erb +3 -3
- data/lib/bowtie/views/form.erb +12 -11
- data/lib/bowtie/views/index.erb +3 -3
- data/lib/bowtie/views/row.erb +15 -16
- data/lib/bowtie/views/search.erb +2 -2
- data/lib/bowtie/views/show.erb +1 -1
- data/lib/bowtie/views/table.erb +17 -14
- metadata +8 -55
- data/README.rdoc +0 -48
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
## Bowtie: Zeroconf admin scaffold for your MongoMapper & DataMapper models
|
2
|
+
|
3
|
+
Bowtie reads the information on your models and creates a nice panel in which you can view, edit and destroy records easily.
|
4
|
+
|
5
|
+
## How does it look?
|
6
|
+
|
7
|
+
![Bowtie!](https://github.com/tomas/bowtie/raw/master/screenshot.png)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Include it in your Gemfile and update your bundle:
|
12
|
+
|
13
|
+
source 'rubygems.org'
|
14
|
+
..
|
15
|
+
gem 'bowtie'
|
16
|
+
|
17
|
+
Or install it by hand:
|
18
|
+
|
19
|
+
$ gem install bowtie
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
Mount Bowtie wherever you want by editing your config.ru file, after loading your models. You can optionally include the admin/pass combination for the panel.
|
24
|
+
|
25
|
+
require 'my_app' # models are loaded
|
26
|
+
require 'bowtie'
|
27
|
+
|
28
|
+
BOWTIE_AUTH = {:user => 'admin', :pass => '12345' }
|
29
|
+
|
30
|
+
app = Rack::Builder.new {
|
31
|
+
map "/admin" do
|
32
|
+
run Bowtie::Admin
|
33
|
+
end
|
34
|
+
|
35
|
+
map '/' do
|
36
|
+
run MyApp
|
37
|
+
end
|
38
|
+
}
|
39
|
+
|
40
|
+
run app
|
41
|
+
|
42
|
+
Now you can go to /admin in your app's path and try out Bowtie using your user/pass combination. If not set, it defaults to admin/bowtie.
|
43
|
+
|
44
|
+
## Important notes
|
45
|
+
|
46
|
+
Bowtie requires a few gems but they're not included in the gemspec to prevent forcing your from installing unneeded gems. Therefore you need to make sure that Bowtie will have the following gems to work with:
|
47
|
+
|
48
|
+
For DataMapper models:
|
49
|
+
|
50
|
+
* dm-core
|
51
|
+
* dm-validations
|
52
|
+
* dm-aggregates
|
53
|
+
* dm-pager
|
54
|
+
|
55
|
+
For MongoMapper models:
|
56
|
+
|
57
|
+
* mongo_mapper
|
58
|
+
|
59
|
+
From version 0.3, Bowtie is meant to be used from DataMapper 1.0.0 on. For previous versions please install with -v=0.2.5.
|
60
|
+
|
61
|
+
## TODO
|
62
|
+
|
63
|
+
* Better handling of types (Text, JSON, IPAddress) in #show
|
64
|
+
* Better handling of relationships in #show
|
65
|
+
|
66
|
+
## Copyright
|
67
|
+
|
68
|
+
(c) 2010-2011 - Tomás Pollak for Fork Ltd. Released under the MIT license.
|
data/bowtie.gemspec
CHANGED
@@ -1,21 +1,48 @@
|
|
1
|
-
|
2
1
|
Gem::Specification.new do |s|
|
3
2
|
s.name = %q{bowtie}
|
4
|
-
s.version = "0.
|
3
|
+
s.version = "0.4"
|
5
4
|
|
6
5
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
7
6
|
s.authors = ["Tomás Pollak"]
|
8
7
|
s.date = %q{2010-06-06}
|
9
|
-
s.description = %q{
|
8
|
+
s.description = %q{Simple admin scaffold for MongoMapper and DataMapper models.}
|
10
9
|
s.email = %q{tomas@forkhq.com}
|
11
|
-
s.extra_rdoc_files = [
|
12
|
-
|
10
|
+
s.extra_rdoc_files = [ "lib/bowtie.rb",
|
11
|
+
"lib/bowtie/admin.rb",
|
12
|
+
"lib/bowtie/core_extensions.rb",
|
13
|
+
"lib/bowtie/helpers.rb" ]
|
14
|
+
|
15
|
+
s.files = [ "README.md",
|
16
|
+
"bowtie.gemspec",
|
17
|
+
"lib/bowtie.rb",
|
18
|
+
"lib/bowtie/adapters/datamapper.rb",
|
19
|
+
"lib/bowtie/adapters/mongomapper.rb",
|
20
|
+
"lib/bowtie/admin.rb",
|
21
|
+
"lib/bowtie/core_extensions.rb",
|
22
|
+
"lib/bowtie/helpers.rb",
|
23
|
+
"lib/bowtie/views/errors.erb",
|
24
|
+
"lib/bowtie/views/field.erb",
|
25
|
+
"lib/bowtie/views/flash.erb",
|
26
|
+
"lib/bowtie/views/form.erb",
|
27
|
+
"lib/bowtie/views/index.erb",
|
28
|
+
"lib/bowtie/views/layout.erb",
|
29
|
+
"lib/bowtie/views/new.erb",
|
30
|
+
"lib/bowtie/views/search.erb",
|
31
|
+
"lib/bowtie/views/show.erb",
|
32
|
+
"lib/bowtie/views/subtypes.erb",
|
33
|
+
"lib/bowtie/views/table.erb",
|
34
|
+
"lib/bowtie/views/row.erb",
|
35
|
+
"lib/bowtie/public/css/bowtie.css",
|
36
|
+
"lib/bowtie/public/js/bowtie.js",
|
37
|
+
"lib/bowtie/public/js/jquery.tablesorter.pack.js",
|
38
|
+
"lib/bowtie/public/js/jquery.jeditable.pack.js" ]
|
39
|
+
|
13
40
|
s.homepage = %q{http://github.com/tomas/bowtie}
|
14
41
|
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Bowtie", "--main", "README"]
|
15
42
|
s.require_paths = ["lib"]
|
16
43
|
s.rubyforge_project = %q{bowtie}
|
17
44
|
s.rubygems_version = %q{1.3.5}
|
18
|
-
s.summary = %q{Bowtie Admin}
|
45
|
+
s.summary = %q{Bowtie Admin Scaffold}
|
19
46
|
|
20
47
|
if s.respond_to? :specification_version then
|
21
48
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
@@ -23,19 +50,10 @@ Gem::Specification.new do |s|
|
|
23
50
|
|
24
51
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
25
52
|
s.add_runtime_dependency(%q<sinatra>, [">= 0.9.4"])
|
26
|
-
s.add_runtime_dependency(%q<dm-core>, [">= 0.10.2"])
|
27
|
-
s.add_runtime_dependency(%q<dm-aggregates>, [">= 0.10.2"])
|
28
|
-
s.add_runtime_dependency(%q<dm-pager>, [">= 1.0.1"])
|
29
53
|
else
|
30
54
|
s.add_dependency(%q<sinatra>, [">= 0.9.4"])
|
31
|
-
s.add_dependency(%q<dm-core>, [">= 0.10.2"])
|
32
|
-
s.add_dependency(%q<dm-aggregates>, [">= 0.10.2"])
|
33
|
-
s.add_dependency(%q<dm-pager>, [">= 1.0.1"])
|
34
55
|
end
|
35
56
|
else
|
36
57
|
s.add_dependency(%q<sinatra>, [">= 0.9.4"])
|
37
|
-
s.add_dependency(%q<dm-core>, [">= 0.10.2"])
|
38
|
-
s.add_dependency(%q<dm-aggregates>, [">= 0.10.2"])
|
39
|
-
s.add_dependency(%q<dm-pager>, [">= 1.0.1"])
|
40
58
|
end
|
41
59
|
end
|
data/lib/bowtie.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
-
libdir = File.dirname(__FILE__)
|
1
|
+
libdir = File.expand_path(File.dirname(__FILE__)) + '/bowtie'
|
2
2
|
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
if defined?(DataMapper)
|
5
|
+
begin
|
6
|
+
%w(dm-core dm-validations dm-aggregates dm-pager json adapters/datamapper).each {|lib| require lib}
|
7
|
+
rescue Gem::LoadError => e
|
8
|
+
raise Gem::LoadError, "Seems you have an outdated version of one of the following gems: dm-core dm-validations dm-aggregates dm-pager"
|
9
|
+
rescue LoadError => e
|
10
|
+
raise Gem::LoadError, "Error loading DataMapper gems. Make sure the all the following gems are available: dm-core dm-validations dm-aggregates dm-pager"
|
11
|
+
end
|
12
|
+
elsif defined?(MongoMapper)
|
13
|
+
%w(mongo_mapper adapters/mongomapper).each {|lib| require lib}
|
14
|
+
else
|
15
|
+
raise Gem::LoadError, "No adapters found. You need to require MongoMapper (mongo_mapper) or DataMapper (dm-core) before requiring Bowtie."
|
10
16
|
end
|
17
|
+
|
18
|
+
%w(sinatra helpers core_extensions admin).each {|lib| require lib}
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Bowtie
|
2
|
+
|
3
|
+
def self.models
|
4
|
+
DataMapper::Model.descendants.to_a
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.search(params, page)
|
8
|
+
query1, query2 = [], []
|
9
|
+
params.each do |key, val|
|
10
|
+
query1 << "#{model}.all(:#{key} => '#{val}')"
|
11
|
+
end
|
12
|
+
model.searchable_fields.each do |field|
|
13
|
+
query2 << "#{model}.all(:#{field}.like => '%#{params[:q]}%')"
|
14
|
+
end
|
15
|
+
query = query1.any? ? [query1.join(' & '), query2.join(' + ')].join(' & ') : query2.join(' + ')
|
16
|
+
@resources = eval(query).page(page, :per_page => PER_PAGE)
|
17
|
+
@subtypes = model.subtypes
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get_many(model, params, page)
|
21
|
+
add_paging(model.all(params), page)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.get_one(model, id)
|
25
|
+
model.get(id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.create(model, params)
|
29
|
+
model.create(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.get_associated(model, params)
|
33
|
+
res = model.get(params[:id]).send(params[:association])
|
34
|
+
# if model.associations[params[:association]].type == :many
|
35
|
+
# add_paging(res, params)
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
|
39
|
+
# doesnt trigger validations or callbacks
|
40
|
+
def self.update!(resource, params)
|
41
|
+
resource.update!(params)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.update(resource, params)
|
45
|
+
resource.update(params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.add_paging(resources, page)
|
49
|
+
resources.respond_to?(:page) ? resources.page(page, :per_page => PER_PAGE) : resources
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.belongs_to_association?(assoc)
|
53
|
+
assoc.class == DataMapper::Associations::ManyToOne::Relationship
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.has_one_association?(assoc)
|
57
|
+
assoc.class == DataMapper::Associations::OneToOne::Relationship
|
58
|
+
end
|
59
|
+
|
60
|
+
module Helpers
|
61
|
+
|
62
|
+
def total_entries(resources)
|
63
|
+
resources.respond_to?(:pager) ? resources.pager.total : resources.count
|
64
|
+
end
|
65
|
+
|
66
|
+
def show_pager(resources, path)
|
67
|
+
resources.pager.to_html(base_path + path) if resources.respond_to?(:pager)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
class Object
|
75
|
+
def primary_key
|
76
|
+
send(self.class.primary_key)
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_json
|
80
|
+
attributes.to_json
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class Class
|
86
|
+
|
87
|
+
def primary_key
|
88
|
+
key.first.name
|
89
|
+
end
|
90
|
+
|
91
|
+
def model_associations
|
92
|
+
h = {}
|
93
|
+
relationships.map {|r| h[r.name] = r }
|
94
|
+
h
|
95
|
+
end
|
96
|
+
|
97
|
+
def field_names
|
98
|
+
self.properties.collect{|p| p.name }
|
99
|
+
end
|
100
|
+
|
101
|
+
def boolean_fields
|
102
|
+
self.properties.map{|a| a.name if a.class == DataMapper::Property::Boolean}.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
def searchable_fields
|
106
|
+
self.properties.map{|a| a.name if a.class == DataMapper::Property::String}.compact
|
107
|
+
end
|
108
|
+
|
109
|
+
def subtypes
|
110
|
+
begin
|
111
|
+
self.validators.first.last.map{|a,b| b = {a.field_name => a.options[:set]} if a.class == DataMapper::Validate::WithinValidator}.compact
|
112
|
+
rescue NoMethodError
|
113
|
+
# puts ' -- dm-validations gem not included. Cannot check subtypes for class.'
|
114
|
+
[]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def options_for_subtype(field)
|
119
|
+
self.validators.first.last.map{|a| a.options[:set] if a.class == DataMapper::Validate::WithinValidator && a.field_name == field}.compact.reduce
|
120
|
+
end
|
121
|
+
|
122
|
+
def relation_keys_include?(property)
|
123
|
+
self.relationships.map {|rel| true if property.to_sym == rel[1].child_key.first.name}.reduce
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Bowtie
|
2
|
+
|
3
|
+
def self.models
|
4
|
+
models = MongoMapper::Document.descendants.to_a.uniq
|
5
|
+
# models.each {|m| models = models + m.subclasses}
|
6
|
+
# models
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.search(params, page)
|
10
|
+
puts "Search not implemented yet in MongoMapper!"
|
11
|
+
return []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get_many(model, params, page)
|
15
|
+
add_paging(model.where(params), page)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.get_one(model, id)
|
19
|
+
model.find(id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create(model, params)
|
23
|
+
model.create(params)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_associated(model, params)
|
27
|
+
model.find(params[:id]).send(params[:association])
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.add_paging(resources, page)
|
31
|
+
resources.skip((page.to_i || 0)*PER_PAGE).limit(PER_PAGE).sort('id asc')
|
32
|
+
end
|
33
|
+
|
34
|
+
# doesnt trigger validations or callbacks
|
35
|
+
def self.update!(resource, params)
|
36
|
+
resource.update_attributes!(params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.update(resource, params)
|
40
|
+
resource.update_attributes(params)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.belongs_to_association?(assoc)
|
44
|
+
assoc.class == MongoMapper::Plugins::Associations::BelongsToAssociation
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.has_one_association?(assoc)
|
48
|
+
assoc.class == MongoMapper::Plugins::Associations::OneAssociation
|
49
|
+
end
|
50
|
+
|
51
|
+
module Helpers
|
52
|
+
|
53
|
+
def total_entries(resources)
|
54
|
+
resources.count
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_page(counter)
|
58
|
+
i = (params[:page].to_i || 0) + counter
|
59
|
+
i == 0 ? '' : "?page=#{i}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def show_pager(resources, path)
|
63
|
+
path = base_path + path.gsub(/[?|&]page=\d+/,'') # remove page from path
|
64
|
+
nextlink = "<li><a class='next' href='#{path}#{get_page(1)}'>Next →</a></li>"
|
65
|
+
prevlink = "<li><a class='prev' href='#{path}#{get_page(-1)}'>← Prev</a></li>"
|
66
|
+
s = params[:page] ? prevlink + nextlink : nextlink
|
67
|
+
"<ul class='pager'>" + s + "</ul>"
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
class Object
|
75
|
+
|
76
|
+
def primary_key
|
77
|
+
send(self.class.primary_key)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
class Class
|
83
|
+
|
84
|
+
def primary_key
|
85
|
+
'id'
|
86
|
+
end
|
87
|
+
|
88
|
+
def model_associations
|
89
|
+
associations
|
90
|
+
end
|
91
|
+
|
92
|
+
def field_names
|
93
|
+
self.keys.keys.collect { |f| f.to_sym }
|
94
|
+
end
|
95
|
+
|
96
|
+
def boolean_fields
|
97
|
+
s = []
|
98
|
+
self.keys.each {|k,v| s << k if v.type == Boolean}
|
99
|
+
s.compact
|
100
|
+
end
|
101
|
+
|
102
|
+
def searchable_fields
|
103
|
+
s = []
|
104
|
+
self.keys.each {|k,v| s << k if v.type == String}
|
105
|
+
s.compact
|
106
|
+
end
|
107
|
+
|
108
|
+
def subtypes
|
109
|
+
s = []
|
110
|
+
self.keys.each {|k,v| s << k if v.type.class == Array}
|
111
|
+
s.compact
|
112
|
+
end
|
113
|
+
|
114
|
+
def options_for_subtype(field)
|
115
|
+
self.keys[field].type
|
116
|
+
end
|
117
|
+
|
118
|
+
def relation_keys_include?(key)
|
119
|
+
self.associations.map {|rel| true if key.to_sym == rel[0]}.reduce
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/lib/bowtie/admin.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Bowtie
|
2
2
|
|
3
|
-
|
3
|
+
PER_PAGE = 25
|
4
4
|
|
5
|
-
|
5
|
+
class Admin < Sinatra::Base
|
6
6
|
|
7
7
|
use Rack::Auth::Basic do |username, password|
|
8
8
|
begin
|
@@ -25,7 +25,7 @@ module Bowtie
|
|
25
25
|
|
26
26
|
before do
|
27
27
|
@app_name = ENV['APP_NAME'] ? [self.class.name, ENV['APP_NAME']].join(' > ') : self.class.name
|
28
|
-
@models =
|
28
|
+
@models = Bowtie.models
|
29
29
|
end
|
30
30
|
|
31
31
|
get '/*.js|css|png|jpg' do
|
@@ -34,30 +34,22 @@ module Bowtie
|
|
34
34
|
|
35
35
|
get '/' do
|
36
36
|
# redirect '' results in an endless redirect on the current version of sinatra/rack
|
37
|
-
redirect '/' + @models.first.
|
37
|
+
redirect '/' + @models.first.linkable
|
38
38
|
end
|
39
39
|
|
40
40
|
get '' do
|
41
|
-
redirect '/' + @models.first.
|
41
|
+
redirect '/' + @models.first.linkable
|
42
42
|
end
|
43
43
|
|
44
44
|
get '/search*' do
|
45
45
|
redirect('/' + params[:model] ||= '') if params[:q].blank?
|
46
|
-
|
47
|
-
clean_params.each do |key, val|
|
48
|
-
query1 << "#{model}.all(:#{key} => '#{val}')"
|
49
|
-
end
|
50
|
-
model.searchable_fields.each do |field|
|
51
|
-
query2 << "#{model}.all(:#{field}.like => '%#{params[:q]}%')"
|
52
|
-
end
|
53
|
-
query = query1.any? ? [query1.join(' & '), query2.join(' + ')].join(' & ') : query2.join(' + ')
|
54
|
-
@resources = eval(query).page(params[:page], :per_page => PER_PAGE)
|
46
|
+
@resources = Bowtie.search(clean_params, params[:page])
|
55
47
|
@subtypes = model.subtypes
|
56
48
|
erb :index
|
57
49
|
end
|
58
50
|
|
59
51
|
get "/:model" do
|
60
|
-
@resources =
|
52
|
+
@resources = Bowtie.get_many(model, clean_params, params[:page])
|
61
53
|
@subtypes = model.subtypes
|
62
54
|
erb :index
|
63
55
|
end
|
@@ -68,9 +60,9 @@ module Bowtie
|
|
68
60
|
end
|
69
61
|
|
70
62
|
post "/:model" do
|
71
|
-
@resource =
|
63
|
+
@resource = Bowtie.create(model, params[:resource].prepare_for_query(model))
|
72
64
|
if @resource.valid? and @resource.save
|
73
|
-
redirect "/#{model.
|
65
|
+
redirect "/#{model.linkable}?notice=created"
|
74
66
|
else
|
75
67
|
erb :new
|
76
68
|
end
|
@@ -83,27 +75,32 @@ module Bowtie
|
|
83
75
|
|
84
76
|
get "/:model/:id/:association" do
|
85
77
|
@title = "#{params[:association].titleize} for #{model.to_s.titleize} ##{params[:id]}"
|
86
|
-
res =
|
87
|
-
|
88
|
-
|
78
|
+
res = Bowtie.get_associated(model, params)
|
79
|
+
|
80
|
+
redirect('/' + model.linkable + '?error=doesnt+exist') if res.nil? or (res.is_a?(Array) and res.empty?)
|
81
|
+
|
82
|
+
if res.is_a?(Array)
|
83
|
+
@resources = Bowtie.add_paging(res, params[:page])
|
89
84
|
erb :index
|
90
85
|
else
|
91
|
-
redirect('/' + model.to_s + '?error=doesnt+exist') unless res
|
92
86
|
@resource = res
|
93
87
|
erb :show
|
94
88
|
end
|
89
|
+
|
95
90
|
end
|
96
91
|
|
97
92
|
put "/:model/:id" do
|
98
93
|
if request.xhr? # dont pass through hooks or put the boolean stuff
|
99
|
-
if
|
94
|
+
# if Bowtie.update!(resource, params[:resource].normalize)
|
95
|
+
puts params[:resource].inspect
|
96
|
+
if Bowtie.update!(resource, params[:resource].filter_inaccessible_in(model).normalize)
|
100
97
|
resource.to_json
|
101
98
|
else
|
102
99
|
false
|
103
100
|
end
|
104
101
|
else # normal request
|
105
|
-
if
|
106
|
-
redirect("/#{model.
|
102
|
+
if Bowtie.update(resource, params[:resource].prepare_for_query(model))
|
103
|
+
redirect("/#{model.linkable}/#{params[:id]}?notice=updated")
|
107
104
|
else
|
108
105
|
@resource = resource
|
109
106
|
erb :show
|
@@ -113,11 +110,12 @@ module Bowtie
|
|
113
110
|
|
114
111
|
delete "/:model/:id" do
|
115
112
|
if resource.destroy
|
116
|
-
redirect "/#{model.
|
113
|
+
redirect "/#{model.linkable}?notice=destroyed"
|
117
114
|
else
|
118
|
-
redirect "/#{model.
|
115
|
+
redirect "/#{model.linkable}/#{params[:id]}?error=not+destroyed"
|
119
116
|
end
|
120
117
|
end
|
118
|
+
|
121
119
|
end
|
122
120
|
|
123
121
|
end
|
@@ -14,39 +14,35 @@ class String
|
|
14
14
|
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
17
|
+
module Subclasses
|
18
|
+
# return a list of the subclasses of a class
|
19
|
+
def subclasses(direct = false)
|
20
|
+
classes = []
|
21
|
+
if direct
|
22
|
+
ObjectSpace.each_object(Class) do |c|
|
23
|
+
next unless c.superclass == self
|
24
|
+
classes << c
|
25
|
+
end
|
26
|
+
else
|
27
|
+
ObjectSpace.each_object(Class) do |c|
|
28
|
+
next unless c.ancestors.include?(self) and (c != self)
|
29
|
+
classes << c
|
30
|
+
end
|
31
|
+
end
|
32
|
+
classes
|
29
33
|
end
|
34
|
+
end
|
30
35
|
|
31
|
-
|
32
|
-
self.properties.map{|a| a.name if a.class == DataMapper::Property::String}.compact
|
33
|
-
end
|
36
|
+
Object.send(:include, Subclasses)
|
34
37
|
|
35
|
-
|
36
|
-
begin
|
37
|
-
self.validators.first.last.map{|a,b| b = {a.field_name => a.options[:set]} if a.class == DataMapper::Validate::WithinValidator}.compact
|
38
|
-
rescue NoMethodError
|
39
|
-
# puts ' -- dm-validations gem not included. Cannot check subtypes for class.'
|
40
|
-
[]
|
41
|
-
end
|
42
|
-
end
|
38
|
+
class Class
|
43
39
|
|
44
|
-
def
|
45
|
-
self.
|
40
|
+
def linkable
|
41
|
+
self.to_s.downcase.pluralize
|
46
42
|
end
|
47
43
|
|
48
|
-
def
|
49
|
-
|
44
|
+
def pluralize
|
45
|
+
self.to_s.pluralize
|
50
46
|
end
|
51
47
|
|
52
48
|
end
|
@@ -71,7 +67,7 @@ class Hash
|
|
71
67
|
|
72
68
|
# this is for checkboxes which give us a param of 'on' on the params hash
|
73
69
|
def normalize
|
74
|
-
replacements = { 'on' => true, '' => nil}
|
70
|
+
replacements = { 'on' => true, '' => nil, 'true' => true, 'false' => false}
|
75
71
|
normalized = {}
|
76
72
|
self.each_pair do |key,val|
|
77
73
|
normalized[key] = replacements.has_key?(val) ? replacements[val] : val
|
data/lib/bowtie/helpers.rb
CHANGED
@@ -38,7 +38,7 @@ module Bowtie
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def resource
|
41
|
-
|
41
|
+
Bowtie.get_one(model, params[:id]) or halt(404, 'Resource not found!')
|
42
42
|
end
|
43
43
|
|
44
44
|
def current_model
|
@@ -56,14 +56,34 @@ module Bowtie
|
|
56
56
|
base_path + '/' + string.to_s.pluralize.downcase
|
57
57
|
end
|
58
58
|
|
59
|
-
def url_for(
|
60
|
-
model_path(
|
59
|
+
def url_for(resource)
|
60
|
+
model_path(resource.class) + '/' + resource.id.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def link_to(string, resource)
|
64
|
+
uri = resource.nil? ? "#" : url_for(resource)
|
65
|
+
"<a href='#{uri}'>#{string}</a>"
|
61
66
|
end
|
62
67
|
|
63
68
|
def truncate(str, length)
|
64
69
|
str.to_s.length > length ? str.to_s[0..length] + '…' : str.to_s
|
65
70
|
end
|
66
71
|
|
72
|
+
def render_assoc_header(rel_name, assoc)
|
73
|
+
"<th title='#{assoc.class.name.to_s[/.*::(.*)$/, 1]}' class='rel-col #{rel_name}-col'>#{rel_name.to_s.titleize}</th>"
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_assoc_row(r, rel_name, assoc)
|
77
|
+
html = "<td class='rel-col #{rel_name.to_s}-col'>"
|
78
|
+
html += "<a href='#{model_path}/#{r.id}/#{rel_name.to_s}'>"
|
79
|
+
if Bowtie.has_one_association?(assoc) || Bowtie.belongs_to_association?(assoc)
|
80
|
+
html += (r.send(rel_name).nil? ? 'nil' : "View #{rel_name.to_s}")
|
81
|
+
else
|
82
|
+
html += r.send(rel_name).count.to_s
|
83
|
+
end
|
84
|
+
html += "</a></td>"
|
85
|
+
end
|
86
|
+
|
67
87
|
end
|
68
88
|
|
69
89
|
end
|
@@ -86,14 +86,47 @@ h2 { margin:10px 0; font-size:130%;}
|
|
86
86
|
-moz-border-top-left-radius: 6px;
|
87
87
|
}
|
88
88
|
|
89
|
-
#header ul li a:hover {
|
90
|
-
|
89
|
+
#header ul li a:hover {
|
90
|
+
background:#333;
|
91
|
+
}
|
92
|
+
|
93
|
+
#header ul li.current a {
|
94
|
+
background: #528e00;
|
95
|
+
font-weight:bold;
|
96
|
+
color:#fff;
|
97
|
+
}
|
98
|
+
|
99
|
+
.subnav {
|
100
|
+
padding: 2px 50px 7px 50px;
|
101
|
+
background:#528e00;
|
102
|
+
font-size:90%;
|
103
|
+
margin: -20px -50px 20px;
|
104
|
+
}
|
91
105
|
|
92
|
-
.subnav {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
.subnav li a
|
106
|
+
.subnav li {
|
107
|
+
display:inline;
|
108
|
+
}
|
109
|
+
|
110
|
+
.subnav li a {
|
111
|
+
color:#fff;
|
112
|
+
text-decoration:none;
|
113
|
+
margin-right:10px;
|
114
|
+
display:inline-block;
|
115
|
+
background:#89bf41;
|
116
|
+
padding:5px;
|
117
|
+
-webkit-border-radius:3px;
|
118
|
+
-moz-border-radius:3px;
|
119
|
+
}
|
120
|
+
|
121
|
+
.subnav li.current a {
|
122
|
+
background: #fff;
|
123
|
+
font-weight:bold;
|
124
|
+
color:#528e00;
|
125
|
+
}
|
126
|
+
|
127
|
+
.subnav li a:active {
|
128
|
+
background:#8def08;
|
129
|
+
}
|
97
130
|
|
98
131
|
/* container
|
99
132
|
-------------------------------------------------------------*/
|
@@ -183,6 +216,12 @@ table .check-column{
|
|
183
216
|
text-align: center;
|
184
217
|
}
|
185
218
|
|
219
|
+
table .view-col,
|
220
|
+
table .delete-col{
|
221
|
+
text-align: center;
|
222
|
+
width: 100px;
|
223
|
+
}
|
224
|
+
|
186
225
|
/* good states
|
187
226
|
---------------------------*/
|
188
227
|
|
@@ -227,12 +266,14 @@ table tr.suspended td{
|
|
227
266
|
background-color: #FFEFEF;
|
228
267
|
}
|
229
268
|
|
230
|
-
table th.rel{
|
269
|
+
table th.rel-col{
|
231
270
|
background: #999;
|
232
271
|
color: #fff;
|
272
|
+
text-align: center;
|
233
273
|
}
|
234
|
-
table td.rel {
|
274
|
+
table td.rel-col {
|
235
275
|
background: #eee;
|
276
|
+
text-align: center;
|
236
277
|
}
|
237
278
|
|
238
279
|
table .icon{
|
data/lib/bowtie/views/errors.erb
CHANGED
data/lib/bowtie/views/field.erb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
<%=
|
2
2
|
|
3
3
|
html =''
|
4
|
-
name = @p
|
4
|
+
name = @p
|
5
5
|
begin
|
6
|
-
value = @resource.send(@p
|
6
|
+
value = @resource.send(@p)
|
7
7
|
rescue NoMethodError
|
8
8
|
value = ''
|
9
9
|
end
|
@@ -20,7 +20,7 @@
|
|
20
20
|
checked = value == true ? 'checked="checked"' : ''
|
21
21
|
html += '<input name="resource['+name+']" type="checkbox" '+checked+' />'
|
22
22
|
else
|
23
|
-
html += '<input class="string" type="text" name="resource['+name+']" value="'+value.to_s+'" />'
|
23
|
+
html += '<input class="string" type="text" name="resource['+name.to_s+']" value="'+value.to_s+'" />'
|
24
24
|
end
|
25
25
|
html
|
26
26
|
|
data/lib/bowtie/views/form.erb
CHANGED
@@ -1,22 +1,23 @@
|
|
1
|
-
<form class="resource" method="post" action="<%= model_path(@resource.
|
1
|
+
<form class="resource" method="post" action="<%= model_path(@resource.class) %><%= '/' + @resource.id.to_s if @resource.id %>">
|
2
2
|
<%= '<input type="hidden" name="_method" value="put">' if @resource.id %>
|
3
3
|
|
4
4
|
<%= partial(:errors) %>
|
5
5
|
|
6
|
-
<table>
|
7
|
-
|
8
|
-
|
9
|
-
<% @
|
6
|
+
<table>
|
7
|
+
|
8
|
+
<!-- filter id and created at fields. association ids pass -->
|
9
|
+
<% @resource.class.field_names.each do |@p| %>
|
10
|
+
<% next if %w(id _id created_at _created_at _type).include?(@p.to_s) %>
|
10
11
|
<tr>
|
11
|
-
<td class="left-col"><%= p.
|
12
|
+
<td class="left-col"><%= @p.to_s.titleize %> <small>(<%= @p.class.name.to_s.gsub("DataMapper::Property::",'') %>)</small></td>
|
12
13
|
<td class="right-col"><%= partial(:field) %></td>
|
13
14
|
</tr>
|
14
15
|
<% end %>
|
15
|
-
<% end %>
|
16
|
-
</table>
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
</table>
|
18
|
+
|
19
|
+
<p class="submit">
|
20
|
+
<input type="submit" value="Submit" />
|
21
|
+
</p>
|
21
22
|
|
22
23
|
</form>
|
data/lib/bowtie/views/index.erb
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
<%= partial(:search) %>
|
4
4
|
<a class="edit-button" href="#" title="Enables in-place editing of editable fields" onclick="Bowtie.toggleEditableMode(this); return false;">Edit Mode OFF</a>
|
5
5
|
|
6
|
-
<h1><%= @resources
|
6
|
+
<h1><%= total_entries(@resources) %> <%= @title || @model.name.pluralize %> <a href="<%= model_path %>/new">(new)</a></h1>
|
7
7
|
|
8
8
|
<% if @resources.any? %>
|
9
9
|
|
10
|
-
<%= @resources
|
10
|
+
<%= show_pager(@resources, request.path_info) %>
|
11
11
|
|
12
12
|
<%= partial(:table) %>
|
13
13
|
|
14
|
-
<%= @resources
|
14
|
+
<%= show_pager(@resources, request.path_info) %>
|
15
15
|
|
16
16
|
<% else %>
|
17
17
|
|
data/lib/bowtie/views/row.erb
CHANGED
@@ -1,25 +1,24 @@
|
|
1
|
-
<tr id="resource-<%= @r.
|
2
|
-
<td class="<%= @model.
|
3
|
-
|
4
|
-
<%
|
5
|
-
|
1
|
+
<tr id="resource-<%= @r.primary_key.to_s %>" class="<%= @r.state.to_s if @r.respond_to?(:state) %>">
|
2
|
+
<td class="<%= @model.primary_key %>-col"><a href="<%= url_for(@r) %>"><%= @r.primary_key %></a></td>
|
3
|
+
|
4
|
+
<% @r.attributes.each do |k, v| %>
|
5
|
+
<% next if k == @model.primary_key or k.to_s[/id$/] or k == '_type' %>
|
6
|
+
<td class="<%= k %>-col <%= k.class.name.downcase %> editable"><%= truncate(v, 64) || 'nil' %></td>
|
6
7
|
<% end %>
|
8
|
+
|
7
9
|
<% unless params[:association] %>
|
8
|
-
<% @model.
|
9
|
-
|
10
|
-
<td class="rel <%= rel[0].to_s %>-col">
|
11
|
-
<a href="<%= model_path %>/<%= @r.id %>/<%= rel[0].to_s %>">
|
12
|
-
<%= rel[1].class.name =~ /OneToOne/ ? "View" : @r.send(rel[0]).count %>
|
13
|
-
</a>
|
14
|
-
</td>
|
15
|
-
<% end %>
|
10
|
+
<% @model.model_associations.each do |rel_name, assoc| %>
|
11
|
+
<%= render_assoc_row(@r, rel_name, assoc) %>
|
16
12
|
<% end %>
|
17
13
|
<% end %>
|
18
|
-
|
19
|
-
<td>
|
20
|
-
|
14
|
+
|
15
|
+
<td class="view-col"><a href="<%= url_for(@r) %>">View</a></td>
|
16
|
+
|
17
|
+
<td class="delete-col">
|
18
|
+
<form class="destroy" method="post" action="<%= model_path(@r.class) %>/<%= @r.id %>" onsubmit="return confirm('Are you sure?');">
|
21
19
|
<input type="hidden" name="_method" value="delete" />
|
22
20
|
<button type="submit">Destroy</button>
|
23
21
|
</form>
|
24
22
|
</td>
|
23
|
+
|
25
24
|
</tr>
|
data/lib/bowtie/views/search.erb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
<% model = params[:model] %>
|
2
2
|
<form id="search" action="<%= base_path %>/search" method="get">
|
3
3
|
<% params.each do |k, v| %>
|
4
|
-
<% next if ['splat', 'q', 'page'].include?(k) or v
|
4
|
+
<% next if ['splat', 'q', 'page'].include?(k) or v == '' %>
|
5
5
|
<input type="hidden" name="<%= k %>" value="<%= v %>" />
|
6
6
|
<% end %>
|
7
|
-
<input type="text" name="q" value="Find
|
7
|
+
<input type="text" name="q" value="<%= params[:q] || 'Find ' + model + ':' %>" onfocus="if(this.value=='Find <%= model %>:')this.value='';" onblur="if(this.value=='')this.value='Find <%= model %>:';"/>
|
8
8
|
</form>
|
data/lib/bowtie/views/show.erb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
<h1><%= @title || @model.name + ':' + @resource.id.to_s %></h1>
|
2
2
|
|
3
|
-
<form class="big destroy" method="post" action="<%= model_path(@resource.
|
3
|
+
<form class="big destroy" method="post" action="<%= model_path(@resource.class) %>/<%= @resource.id %>" onsubmit="return confirm('Are you sure?');">
|
4
4
|
<input type="hidden" name="_method" value="delete" />
|
5
5
|
<button type="submit">Destroy</button>
|
6
6
|
</form>
|
data/lib/bowtie/views/table.erb
CHANGED
@@ -1,25 +1,28 @@
|
|
1
1
|
<table class="sortable">
|
2
2
|
<thead>
|
3
3
|
<tr>
|
4
|
-
<th title="<%= @model.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<% @resources.first.class.relationships.each do |rel| %>
|
11
|
-
<% unless rel[1].class.name =~ /ManyTo/ %>
|
12
|
-
<th title="<%= rel[1].class.name.to_s.gsub('DataMapper::Associations::','') %>" class="rel <%= rel[0].to_s %>-col"><%= rel[0].to_s.titleize %></th>
|
4
|
+
<th title="<%= @model.primary_key %>" class="<%= @model.primary_key %>-col"><%= @model.primary_key.to_s.upcase %></th>
|
5
|
+
|
6
|
+
<!-- filter id, _type and associations -->
|
7
|
+
<% @resources.first.attributes.each do |k, v| %>
|
8
|
+
<% next if k.to_s == 'id' or k.to_s[/id$/] or k == '_type' or v.class == Array %>
|
9
|
+
<th title="<%= v.class %>" class="<%= k.to_s %>-col <%= v.class.name.downcase %>"><%= k.to_s.titleize %></th>
|
13
10
|
<% end %>
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
|
12
|
+
<% unless params[:association] %>
|
13
|
+
<% @resources.first.class.model_associations.each do |rel_name, assoc| %>
|
14
|
+
<%= render_assoc_header(rel_name, assoc) %>
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<th class="actions" colspan="2"> </th>
|
17
19
|
</tr>
|
18
20
|
</thead>
|
21
|
+
|
19
22
|
<tbody>
|
20
|
-
<% @resources.each do
|
21
|
-
<% @r = r %>
|
23
|
+
<% @resources.each do |@r| %>
|
22
24
|
<%= partial(:row) %>
|
23
25
|
<% end %>
|
24
26
|
</tbody>
|
27
|
+
|
25
28
|
</table>
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bowtie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 3
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
|
10
|
-
version: 0.3.2
|
8
|
+
- 4
|
9
|
+
version: "0.4"
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- "Tom\xC3\xA1s Pollak"
|
@@ -34,55 +33,7 @@ dependencies:
|
|
34
33
|
version: 0.9.4
|
35
34
|
type: :runtime
|
36
35
|
version_requirements: *id001
|
37
|
-
|
38
|
-
name: dm-core
|
39
|
-
prerelease: false
|
40
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
|
-
requirements:
|
43
|
-
- - ">="
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
hash: 51
|
46
|
-
segments:
|
47
|
-
- 0
|
48
|
-
- 10
|
49
|
-
- 2
|
50
|
-
version: 0.10.2
|
51
|
-
type: :runtime
|
52
|
-
version_requirements: *id002
|
53
|
-
- !ruby/object:Gem::Dependency
|
54
|
-
name: dm-aggregates
|
55
|
-
prerelease: false
|
56
|
-
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
hash: 51
|
62
|
-
segments:
|
63
|
-
- 0
|
64
|
-
- 10
|
65
|
-
- 2
|
66
|
-
version: 0.10.2
|
67
|
-
type: :runtime
|
68
|
-
version_requirements: *id003
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: dm-pager
|
71
|
-
prerelease: false
|
72
|
-
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
|
-
requirements:
|
75
|
-
- - ">="
|
76
|
-
- !ruby/object:Gem::Version
|
77
|
-
hash: 21
|
78
|
-
segments:
|
79
|
-
- 1
|
80
|
-
- 0
|
81
|
-
- 1
|
82
|
-
version: 1.0.1
|
83
|
-
type: :runtime
|
84
|
-
version_requirements: *id004
|
85
|
-
description: Admin scaffold for DataMapper models, on Sinatra.
|
36
|
+
description: Simple admin scaffold for MongoMapper and DataMapper models.
|
86
37
|
email: tomas@forkhq.com
|
87
38
|
executables: []
|
88
39
|
|
@@ -94,9 +45,11 @@ extra_rdoc_files:
|
|
94
45
|
- lib/bowtie/core_extensions.rb
|
95
46
|
- lib/bowtie/helpers.rb
|
96
47
|
files:
|
97
|
-
- README.
|
48
|
+
- README.md
|
98
49
|
- bowtie.gemspec
|
99
50
|
- lib/bowtie.rb
|
51
|
+
- lib/bowtie/adapters/datamapper.rb
|
52
|
+
- lib/bowtie/adapters/mongomapper.rb
|
100
53
|
- lib/bowtie/admin.rb
|
101
54
|
- lib/bowtie/core_extensions.rb
|
102
55
|
- lib/bowtie/helpers.rb
|
@@ -155,6 +108,6 @@ rubyforge_project: bowtie
|
|
155
108
|
rubygems_version: 1.5.2
|
156
109
|
signing_key:
|
157
110
|
specification_version: 3
|
158
|
-
summary: Bowtie Admin
|
111
|
+
summary: Bowtie Admin Scaffold
|
159
112
|
test_files: []
|
160
113
|
|
data/README.rdoc
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
= Bowtie for Sinatra.
|
2
|
-
|
3
|
-
Zeroconf admin generator for DataMapper models, written in Sinatra.
|
4
|
-
|
5
|
-
= What it does
|
6
|
-
|
7
|
-
Reads the information on your models and creates a nice panel in which you can view, edit and destroy records easily.
|
8
|
-
|
9
|
-
= Installation
|
10
|
-
|
11
|
-
$ sudo gem install bowtie
|
12
|
-
|
13
|
-
= Important
|
14
|
-
|
15
|
-
From version 0.3, Bowtie is meant to be used from DataMapper 1.0.0 on. For previous versions please install with -v=0.2.5.
|
16
|
-
|
17
|
-
= Configuration
|
18
|
-
|
19
|
-
Mount Bowtie wherever you want by editing your config.ru file, optionally including the admin/pass combination for the panel.
|
20
|
-
|
21
|
-
require 'myapp'
|
22
|
-
require 'bowtie'
|
23
|
-
|
24
|
-
BOWTIE_AUTH = {:user => 'admin', :pass => '12345' }
|
25
|
-
|
26
|
-
app = Rack::Builder.new {
|
27
|
-
map "/admin" do
|
28
|
-
run Bowtie::Admin
|
29
|
-
end
|
30
|
-
|
31
|
-
map '/' do
|
32
|
-
run MyApp
|
33
|
-
end
|
34
|
-
}
|
35
|
-
|
36
|
-
run app
|
37
|
-
|
38
|
-
Now you can go to /admin in your app's path and try out Bowtie using your user/pass combination. If not set, it defaults to admin/bowtie.
|
39
|
-
|
40
|
-
= TODO
|
41
|
-
|
42
|
-
* Better handling of types (Text, JSON, IPAddress) in #show
|
43
|
-
* Better handling of relationships in #show
|
44
|
-
|
45
|
-
= Copyright
|
46
|
-
|
47
|
-
(c) 2010 - Tomás Pollak for Fork Ltd.
|
48
|
-
Released under the MIT license.
|