bowtie 0.3.2 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -1,21 +1,48 @@
1
-
2
1
  Gem::Specification.new do |s|
3
2
  s.name = %q{bowtie}
4
- s.version = "0.3.2"
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{Admin scaffold for DataMapper models, on Sinatra.}
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 = ["lib/bowtie.rb", "lib/bowtie/admin.rb", "lib/bowtie/core_extensions.rb", "lib/bowtie/helpers.rb"]
12
- s.files = ["README.rdoc", "bowtie.gemspec", "lib/bowtie.rb", "lib/bowtie/admin.rb", "lib/bowtie/core_extensions.rb", "lib/bowtie/helpers.rb", "lib/bowtie/views/errors.erb", "lib/bowtie/views/field.erb", "lib/bowtie/views/flash.erb", "lib/bowtie/views/form.erb", "lib/bowtie/views/index.erb", "lib/bowtie/views/layout.erb", "lib/bowtie/views/new.erb", "lib/bowtie/views/search.erb", "lib/bowtie/views/search.erb", "lib/bowtie/views/show.erb", "lib/bowtie/views/subtypes.erb", "lib/bowtie/views/table.erb", "lib/bowtie/views/row.erb", "lib/bowtie/public/css/bowtie.css", "lib/bowtie/public/js/bowtie.js", "lib/bowtie/public/js/jquery.tablesorter.pack.js", "lib/bowtie/public/js/jquery.jeditable.pack.js"]
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
@@ -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
- %w(sinatra dm-core dm-aggregates dm-pager bowtie/helpers bowtie/core_extensions bowtie/admin).each {|lib| require lib}
5
-
6
- begin
7
- repository(:default).adapter
8
- rescue DataMapper::RepositoryNotSetupError
9
- puts ' ** You need to set up your DataMapper adapter for Bowtie to work. **'
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 &rarr;</a></li>"
65
+ prevlink = "<li><a class='prev' href='#{path}#{get_page(-1)}'>&larr; 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
@@ -1,8 +1,8 @@
1
1
  module Bowtie
2
2
 
3
- class Admin < Sinatra::Base
3
+ PER_PAGE = 25
4
4
 
5
- PER_PAGE = 25
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 = DataMapper::Model.descendants.to_a
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.pluralize
37
+ redirect '/' + @models.first.linkable
38
38
  end
39
39
 
40
40
  get '' do
41
- redirect '/' + @models.first.pluralize
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
- query1, query2 = [], []
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 = model.all(clean_params).page(params[:page], :per_page => PER_PAGE)
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 = model.create(params[:resource].prepare_for_query(model))
63
+ @resource = Bowtie.create(model, params[:resource].prepare_for_query(model))
72
64
  if @resource.valid? and @resource.save
73
- redirect "/#{model.pluralize}?notice=created"
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 = model.get(params[:id]).send(params[:association])
87
- if res.respond_to?(:page)
88
- @resources = res.page(params[:page], :per_page => PER_PAGE)
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 resource.update!(params[:resource].filter_inaccessible_in(model).normalize)
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 resource.update(params[:resource].prepare_for_query(model))
106
- redirect("/#{model.pluralize}/#{params[:id]}?notice=updated")
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.pluralize}?notice=destroyed"
113
+ redirect "/#{model.linkable}?notice=destroyed"
117
114
  else
118
- redirect "/#{model.pluralize}/#{params[:id]}?error=not+destroyed"
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
- class Class
18
-
19
- def pluralize
20
- self.to_s.pluralize
21
- end
22
-
23
- def field_names
24
- self.properties.collect{|p| p.name }
25
- end
26
-
27
- def boolean_fields
28
- self.properties.map{|a| a.name if a.class == DataMapper::Property::Boolean}.compact
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
- def searchable_fields
32
- self.properties.map{|a| a.name if a.class == DataMapper::Property::String}.compact
33
- end
36
+ Object.send(:include, Subclasses)
34
37
 
35
- def subtypes
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 options_for_subtype(field)
45
- self.validators.first.last.map{|a| a.options[:set] if a.class == DataMapper::Validate::WithinValidator && a.field_name == field}.compact.reduce
40
+ def linkable
41
+ self.to_s.downcase.pluralize
46
42
  end
47
43
 
48
- def relation_keys_include?(property)
49
- self.relationships.map {|rel| true if property.to_sym == rel[1].child_key.first.name}.reduce
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
@@ -38,7 +38,7 @@ module Bowtie
38
38
  end
39
39
 
40
40
  def resource
41
- @resource ||= model.get(params[:id]) or halt(404, 'Resource not found!')
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(object)
60
- model_path(object.class) + '/' + object.id.to_s
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] + '&hellip;' : 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 { background:#333;}
90
- #header ul li.current a { background: #528e00; font-weight:bold; color:#fff;}
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 { padding: 2px 50px 7px 50px; background:#528e00; font-size:90%; margin: -20px -50px 20px; }
93
- .subnav li { display:inline;}
94
- .subnav li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; background:#89bf41; padding:5px; -webkit-border-radius:3px; -moz-border-radius:3px;}
95
- .subnav li.current a { background:#fff; font-weight:bold; color:#528e00;}
96
- .subnav li a:active { background:#8def08;}
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{
@@ -24,7 +24,7 @@ var Bowtie = {
24
24
 
25
25
  activateEditables: function(){
26
26
 
27
- var current_path = location.href.split('#')[0];
27
+ var current_path = location.pathname;
28
28
 
29
29
  $('table td.editable').editable(function(value, settings){
30
30
 
@@ -14,6 +14,6 @@ if @resource and @resource.errors.any? %>
14
14
 
15
15
  <% end
16
16
 
17
- rescue NoMethodError
17
+ rescue NoMethodError => e
18
18
  # dm-validations not included
19
19
  end %>
@@ -1,9 +1,9 @@
1
1
  <%=
2
2
 
3
3
  html =''
4
- name = @p.name.to_s
4
+ name = @p
5
5
  begin
6
- value = @resource.send(@p.name)
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
 
@@ -1,22 +1,23 @@
1
- <form class="resource" method="post" action="<%= model_path(@resource.model) %><%= '/' + @resource.id.to_s if @resource.id %>">
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
- <% @resource.model.properties.each do |p| %>
8
- <% unless [:created_at, :id].include?(p.name) %>
9
- <% @p = p %>
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.name.to_s.titleize %> <small>(<%= p.class.name.to_s.gsub("DataMapper::Property::",'') || 'Enum' %>)</small></td>
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
- <p class="submit">
19
- <input type="submit" value="Submit" />
20
- </p>
17
+ </table>
18
+
19
+ <p class="submit">
20
+ <input type="submit" value="Submit" />
21
+ </p>
21
22
 
22
23
  </form>
@@ -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.pager.total %> <%= @title || @model.name.pluralize %> <a href="<%= model_path %>/new">(new)</a></h1>
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.pager.to_html(@env['REQUEST_URI']) if @resources.respond_to?(:pager) %>
10
+ <%= show_pager(@resources, request.path_info) %>
11
11
 
12
12
  <%= partial(:table) %>
13
13
 
14
- <%= @resources.pager.to_html(@env['REQUEST_URI']) if @resources.respond_to?(:pager) %>
14
+ <%= show_pager(@resources, request.path_info) %>
15
15
 
16
16
  <% else %>
17
17
 
@@ -1,25 +1,24 @@
1
- <tr id="resource-<%= @r.key.first.to_s %>" class="<%= @r.state.to_s if @r.respond_to?(:state) %>">
2
- <td class="<%= @model.key.first.name.to_s %>-col"><a href="<%= url_for(@r) %>"><%= @r.key.first.to_s %></a></td>
3
- <% @r.attributes.each do |a| %>
4
- <% next if a[0] == @model.key.first.name %>
5
- <td class="<%= a[0] %>-col editable"><%= truncate(a[1], 64) || 'nil' %></td>
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.relationships.each do |rel| %>
9
- <% unless rel[1].class.name =~ /ManyTo/ %>
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
- <td><a href="<%= url_for(@r) %>">View</a></td>
19
- <td>
20
- <form class="destroy" method="post" action="<%= model_path(@r.model) %>/<%= @r.id %>" onsubmit="return confirm('Are you sure?');">
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>
@@ -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.blank? %>
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 <%= model %>:" onfocus="if(this.value=='Find <%= model %>:')this.value='';" onblur="if(this.value=='')this.value='Find <%= model %>:';"/>
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>
@@ -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.model) %>/<%= @resource.id %>" onsubmit="return confirm('Are you sure?');">
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>
@@ -1,25 +1,28 @@
1
1
  <table class="sortable">
2
2
  <thead>
3
3
  <tr>
4
- <th title="<%= @model.key.first.type.to_s %>" class="<%= @model.key.first.name.to_s %>-col"><%= @model.key.first.name.to_s.upcase %></th>
5
- <% @resources.first.attributes.each do |p| %>
6
- <% next if p[0] == @model.key.first.name %>
7
- <th title="<%= p[1].class.to_s %>" class="<%= p[0].to_s %>-col"><%= p[0].to_s.titleize %></th>
8
- <% end %>
9
- <% unless params[:association] %>
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
- <% end %>
15
- <% end %>
16
- <th class="actions" colspan="3">&nbsp;</th>
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">&nbsp;</th>
17
19
  </tr>
18
20
  </thead>
21
+
19
22
  <tbody>
20
- <% @resources.each do |r| %>
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: 23
4
+ hash: 3
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 3
9
- - 2
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
- - !ruby/object:Gem::Dependency
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.rdoc
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
 
@@ -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.