togo 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog ADDED
@@ -0,0 +1 @@
1
+ 0.1.0 - Initial Foray
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2010 Matt King
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,23 @@
1
+ == Togo: Automatic Admin for Ruby ORMs
2
+
3
+ With just a few lines of code in your ORM classes, you get a full-featured content administration tool.
4
+
5
+ Quick example for DataMapper:
6
+
7
+ class BlogEntry
8
+
9
+ include DataMapper::Resource
10
+ include Togo::DataMapper::Model
11
+
12
+ property :id, Serial
13
+ property :title, String
14
+ property :body, Text
15
+ property :published, Boolean
16
+
17
+ list_properties :title, :published
18
+
19
+ configure_property :published, :label => "Choose 'yes' to publish blog entry"
20
+
21
+ end
22
+
23
+ Current only works with DataMapper.
data/bin/togo-admin ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ files_to_require = []
5
+
6
+ config = {
7
+ :environment => :development,
8
+ :standalone => true
9
+ }
10
+
11
+ OptionParser.new{|opt|
12
+ opt.on("-p port"){|port| config[:port] = port}
13
+ opt.on("-h host"){|host| config[:host] = host}
14
+ opt.on("-e env"){|env| config[:environment] = env.to_sym}
15
+ opt.on("-a handler"){|handler| config[:handler] = Module.const_get(handler) if defined?(handler)}
16
+ opt.on("-r req"){|req| files_to_require << req}
17
+ opt.on("--help") do
18
+ puts "Usage: togo-admin [options]"
19
+ puts "Options: "
20
+ puts "\t-p [port]: which port to run on"
21
+ puts "\t-p [host]: which host to run on"
22
+ puts "\t-e [environment]: which environment to boot in"
23
+ puts "\t-r [file]: which file to require before running."
24
+ puts "\t--help: what you're seeing"
25
+ puts "TIP: To load code automatically before running togo-admin, create a file called togo-admin-config.rb."
26
+ exit
27
+ end
28
+ }.parse!
29
+
30
+ begin
31
+ files_to_require.each{|f| require f}
32
+ require 'togo-admin-config'
33
+ rescue LoadError => detail
34
+ puts "Warning: #{detail}"
35
+ end
36
+
37
+ require 'togo'
38
+ require 'togo/admin'
39
+
40
+ config.merge!({
41
+ :handler => Togo::Admin.handler,
42
+ :reloader => Togo::TogoReloader
43
+ })
44
+
45
+ Togo::Admin.configure(config)
46
+ Togo::Admin.run!
@@ -0,0 +1,76 @@
1
+ %w(dm-core rack).each{|l| require l}
2
+ Dir.glob(File.join('models','*.rb')).each{|f| require f}
3
+
4
+ module Togo
5
+ class Admin < Dispatch
6
+
7
+ before do
8
+ @model = Togo.const_get(params[:model]) if params[:model]
9
+ end
10
+
11
+ get '/' do
12
+ redirect "/#{Togo.models.first}"
13
+ end
14
+
15
+ get '/:model' do
16
+ @content = params[:q] ? @model.search(:q => params[:q]) : @model.all
17
+ erb :index
18
+ end
19
+
20
+ get '/new/:model' do
21
+ @content = @model.new
22
+ erb :new
23
+ end
24
+
25
+ post '/create/:model' do
26
+ @content = @model.stage_content(@model.new,params)
27
+ begin
28
+ raise "Could not save content" if not @content.save
29
+ redirect "/#{@model.name}"
30
+ rescue => detail
31
+ @errors = detail.to_s
32
+ erb :edit
33
+ end
34
+ end
35
+
36
+ get '/edit/:model/:id' do
37
+ @content = @model.get(params[:id])
38
+ erb :edit
39
+ end
40
+
41
+ post '/update/:model/:id' do
42
+ @content = @model.stage_content(@model.get(params[:id]),params)
43
+ begin
44
+ raise "Could not save content" if not @content.save
45
+ redirect "/#{@model.name}"
46
+ rescue => detail
47
+ @errors = detail.to_s
48
+ erb :edit
49
+ end
50
+ end
51
+
52
+ post '/delete/:model' do
53
+ @items = @model.all(:id => params[:id].split(','))
54
+ begin
55
+ @items.each do |i|
56
+ @model.delete_content(i)
57
+ end
58
+ redirect "/#{@model.name}"
59
+ rescue => detail
60
+ @errors = detail.to_s
61
+ @content = params[:q] ? @model.search(:q => params[:q]) : @model.all
62
+ erb :index
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ # Subclass Rack Reloader to call DataMapper.auto_upgrade! on file reload
69
+ class TogoReloader < Rack::Reloader
70
+ def safe_load(*args)
71
+ super(*args)
72
+ ::DataMapper.auto_upgrade!
73
+ end
74
+ end
75
+
76
+ end
@@ -0,0 +1,208 @@
1
+ * {
2
+ outline: none;
3
+ }
4
+ body {
5
+ font: 76% helvetica;
6
+ padding: 0;
7
+ margin: 0;
8
+ }
9
+ #header {
10
+ background: #4076c7 url(/img/bg-header.png) repeat-x top left;
11
+ height: 24px;
12
+ color: #FFF;
13
+ line-height: 24px;
14
+ font-size: 18px;
15
+ padding: 10px 20px;
16
+ text-shadow: #1E1E1E 1px 1px 2px;
17
+ }
18
+ #nav {
19
+ width: 200px;
20
+ position: absolute;
21
+ left: 0;
22
+ top: 44px;
23
+ bottom: 0;
24
+ overflow: auto;
25
+ background: #FFF url(/img/bg-side.png) repeat-x bottom left;
26
+ }
27
+ #nav ul {
28
+ margin: 0;
29
+ padding: 0;
30
+ }
31
+ #nav ul li {
32
+ margin: 0;
33
+ padding: 0;
34
+ list-style-type: none;
35
+ }
36
+ #nav ul li a {
37
+ padding: 10px 20px;
38
+ display: block;
39
+ color: #000;
40
+ text-decoration: none;
41
+ border-bottom: 1px solid #CECECE;
42
+ background: #FFF url(/img/bg-nav.png) repeat-x bottom left;
43
+ }
44
+ #nav ul li a.selected,
45
+ #nav ul li a:hover {
46
+ background: #1E1E1E url(/img/bg-nav.png) repeat-x top left;
47
+ color: #FFF;
48
+ border-bottom: 1px solid #1E1E1E;
49
+ }
50
+ #main {
51
+ position: absolute;
52
+ top: 44px;
53
+ left: 0;
54
+ right: 0;
55
+ bottom: 0;
56
+ background: #F3F3F3;
57
+ margin-left: 200px;
58
+ border-left: 1px solid #323941;
59
+ overflow: auto;
60
+ }
61
+ #main h1 {
62
+ border-bottom: 1px solid #CECECE;
63
+ background: #FFF url(/img/bg-headline.png) repeat-x bottom left;
64
+ font-weight: normal;
65
+ font-size: 24px;
66
+ line-height: 36px;
67
+ padding: 10px 20px;
68
+ margin: 0;
69
+ }
70
+ #main #search-form {
71
+ position: absolute;
72
+ right: 0;
73
+ z-index: 3;
74
+ width: 270px;
75
+ top: 0;
76
+ }
77
+ #main #search-form fieldset {
78
+ padding: 15px 0;
79
+ }
80
+ #main #search-form input[type=text] {
81
+ width: 190px;
82
+ }
83
+ #main h1 a {
84
+ text-decoration: none;
85
+ color: #4076c7;
86
+ }
87
+ #main table, #main form {
88
+ width: 100%;
89
+ }
90
+ #main table {
91
+ border-collapse: collapse;
92
+ }
93
+ #main table a {
94
+ color: #000;
95
+ text-decoration: none;
96
+ }
97
+ #main table th {
98
+ text-align: left;
99
+ padding: 10px 20px;
100
+ background: #1E1E1E url(/img/bg-nav.png) repeat-x top left;
101
+ color: #FFF;
102
+ }
103
+ #main table td {
104
+ padding: 10px 20px;
105
+ border-bottom: 1px solid #CECECE;
106
+ background: #FFF;
107
+ }
108
+ #main table td.checkbox {
109
+ width: 10px;
110
+ text-align: center;
111
+ padding-right: 0;
112
+ }
113
+
114
+ #main .wrapper {
115
+ position: absolute;
116
+ top: 57px;
117
+ bottom: 47px;
118
+ right: 0;
119
+ left: 0;
120
+ overflow: auto;
121
+ }
122
+ #main form .wrapper {
123
+ padding-top: 20px;
124
+ }
125
+ #main .wrapper .errors {
126
+ padding: 0 20px 10px 20px;
127
+ color: #F00;
128
+ }
129
+ #main form fieldset {
130
+ border: none;
131
+ padding: 0 30px 20px 20px;
132
+ margin: 0;
133
+ }
134
+ #main form fieldset label {
135
+ display: block;
136
+ font-weight: bold;
137
+ padding: 0 0 6px 0;
138
+ }
139
+ #main form fieldset label.inline {
140
+ display: inline;
141
+ font-weight: normal;
142
+ }
143
+ #main form fieldset textarea,
144
+ #main form fieldset input[type=text] {
145
+ border: 1px solid #CECECE;
146
+ width: 100%;
147
+ padding: 5px;
148
+ }
149
+ #main form fieldset textarea:focus,
150
+ #main form fieldset input[type=text]:focus {
151
+ border: 1px solid #ABABAB;
152
+ }
153
+ div.actions {
154
+ position: absolute;
155
+ bottom: 0;
156
+ right: 0;
157
+ left: 0;
158
+ z-index: 3;
159
+ background: #888;
160
+ text-align: right;
161
+ height: 46px;
162
+ border-top: 1px solid #323941;
163
+ }
164
+ div.actions button,
165
+ div.actions a {
166
+ width: 80px;
167
+ height: 22px;
168
+ color: #111;
169
+ text-align: center;
170
+ border: 0;
171
+ cursor: pointer;
172
+ background: transparent url(/img/btn-bg.png) repeat-x top left;
173
+ font: 12px "Lucida Grande", verdana;
174
+ position: absolute;
175
+ top: 12px;
176
+ line-height: 22px;
177
+ right: 10px;
178
+ }
179
+ div.actions a {
180
+ line-height: 23px;
181
+ display: block;
182
+ text-decoration: none;
183
+ overflow: hidden;
184
+ }
185
+ div.actions button {
186
+ font: 12px "Lucida Grande", verdana;
187
+ }
188
+ div.actions button:hover,
189
+ div.actions a:hover {
190
+ background: transparent url(/img/btn-bg.png) repeat-x left -22px;
191
+ }
192
+ div.actions button:disabled {
193
+ color: #999;
194
+ }
195
+ div.actions button:disabled:hover {
196
+ cursor: text;
197
+ background: transparent url(/img/btn-bg.png) repeat-x top left;
198
+ }
199
+ div.actions button#delete-button {
200
+ right: 100px;
201
+ }
202
+ div.external {
203
+ right: 100px;
204
+ background: transparent;
205
+ }
206
+ div.external button#delete-button {
207
+ right: 0;
208
+ }
@@ -0,0 +1,32 @@
1
+ var app = (function() {
2
+
3
+ var selectedItems = [];
4
+
5
+ var handleMultiSelect = function(e) {
6
+ var id = e.target.getAttribute('id').split('_')[1];
7
+ if (e.target.checked) {
8
+ selectedItems.push(id);
9
+ } else {
10
+ for (var i = 0, len = selectedItems.length; i < len; i++) {
11
+ if (selectedItems[i] == id) {
12
+ selectedItems.splice(i,1);
13
+ break;
14
+ }
15
+ }
16
+ }
17
+ if (selectedItems.length > 0) {
18
+ el('delete-button').removeAttribute('disabled');
19
+ } else {
20
+ el('delete-button').setAttribute('disabled','disabled');
21
+ }
22
+ el('delete-list').value = selectedItems.join(',');
23
+ };
24
+
25
+ var c = el('list-table').getElementsByTagName('input');
26
+ for (var i = 0; i < c.length; i++) {
27
+ if (c[i].getAttribute('type') == 'checkbox') {
28
+ c[i].addEventListener('click', handleMultiSelect, false);
29
+ }
30
+ }
31
+
32
+ })();
@@ -0,0 +1,3 @@
1
+ function el(node) {
2
+ return document.getElementById(node);
3
+ }
@@ -0,0 +1,3 @@
1
+ <h2>My Custom Title</h2>
2
+ <label for="<%= property.name %>"><%= property.name %> and stuff</label>
3
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
@@ -0,0 +1,20 @@
1
+ <h1><a href="/<%= @model.name %>"><%= @model.display_name %></a> / Edit</h1>
2
+ <form action="/update/<%= @model.name %>/<%= @content.id %>" method="post">
3
+ <div class="wrapper">
4
+ <% if @errors %><div id="save_errors" class="errors"><%= @errors %></div><% end %>
5
+ <% @model.form_properties.each do |p| %>
6
+ <fieldset id="property_<%= p.name %>">
7
+ <%= @model.form_for(p,@content) %>
8
+ </fieldset>
9
+ <% end %>
10
+ </div>
11
+ <div class="actions">
12
+ <button type="submit" class="save">Save</button>
13
+ </div>
14
+ </form>
15
+ <div class="actions external">
16
+ <form action="/delete/<%= @model.name %>" method="post" id="delete-form">
17
+ <input type="hidden" id="delete-list" name="id" value="<%= @content.id %>" />
18
+ <button type="submit" id="delete-button">Delete</button>
19
+ </form>
20
+ </div>
@@ -0,0 +1,44 @@
1
+ <h1><%= @model.display_name %></h1>
2
+ <form method="get" id="search-form">
3
+ <fieldset>
4
+ <input type="text" name="q" value="<%= params[:q] rescue '' %>" size="40" /> <button type="submit">Search</button>
5
+ </fieldset>
6
+ </form>
7
+ <div class="wrapper">
8
+ <table id="list-table">
9
+ <thead>
10
+ <tr>
11
+ <% if not @content.empty? %>
12
+ <th></th>
13
+ <% end %>
14
+ <% @model.list_properties.each do |p| %>
15
+ <th><%= p.name.to_s.humanize.titleize %></th>
16
+ <% end %>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% if @content.empty? %>
21
+ <tr>
22
+ <td colspan="<%= @model.list_properties.size %>"><% if params[:q] %>No results for '<%= params[:q] %>'<% else %>No content has been created yet.<% end %></td>
23
+ </tr>
24
+ <% else %>
25
+ <% @content.each do |c| %>
26
+ <tr>
27
+ <td class="checkbox"><input type="checkbox" name="selection[<%= c.id %>]" value="1" id="selection_<%= c.id %>" /></td>
28
+ <% @model.list_properties.each_with_index do |p,i| %>
29
+ <td><% if i == 0 %><a href="/edit/<%= @model.name %>/<%= c.id %>"><%= c.send(p.name.to_sym) || '-' %></a><% else %><%= c.send(p.name.to_sym) %><% end %></td>
30
+ <% end %>
31
+ </tr>
32
+ <% end %>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+ </div>
37
+ <div class="actions">
38
+ <a href="/new/<%= @model.name %>" class="action new" id="new-button">New</a>
39
+ <form action="/delete/<%= @model.name %>" method="post" id="delete-form">
40
+ <input type="hidden" id="delete-list" name="id" value="" />
41
+ <button type="submit" id="delete-button" disabled="disabled">Delete</button>
42
+ </form>
43
+ </div>
44
+ <script type="text/javascript" src="/js/index.js"></script>
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4
+ <html xmlns="http://www.w3.org/1999/xhtml">
5
+ <head>
6
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
7
+ <title>Togo Admin</title>
8
+ <link rel="stylesheet" href="/css/screen.css" />
9
+ <script type="text/javascript" src="/js/togo.js"></script>
10
+ </head>
11
+ <body>
12
+ <div id="header">Togo</div>
13
+ <div id="nav">
14
+ <ul>
15
+ <% Togo.models.each do |m| %>
16
+ <li><a href="/<%= m.name %>" class="<%= @model.name == m.name ? 'selected' : '' %>"><%= m.display_name %></a></li>
17
+ <% end %>
18
+ </ul>
19
+ </div>
20
+ <div id="main">
21
+ <%= yield %>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,14 @@
1
+ <h1><a href="/<%= @model.name %>"><%= @model.display_name %></a> / New</h1>
2
+ <form action="/create/<%= @model.name %>" method="post">
3
+ <div class="wrapper">
4
+ <% if @errors %><div id="save_errors" class="errors"><%= @errors %></div><% end %>
5
+ <% @model.form_properties.each do |p| %>
6
+ <fieldset id="field_<%= p.name %>">
7
+ <%= @model.form_for(p,@content) %>
8
+ </fieldset>
9
+ <% end %>
10
+ </div>
11
+ <div class="actions">
12
+ <button type="submit" class="create">Create</button>
13
+ </div>
14
+ </form>
data/lib/togo/admin.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'dispatch'
2
+ require 'togo/admin/admin'
3
+
4
+ Togo::Admin.configure({
5
+ :view_path => File.join(($:.find{|p| p =~ /lib\/togo\/admin/} || '../lib/togo/admin'),'views'),
6
+ :public_path => File.join(($:.find{|p| p =~ /lib\/togo\/admin/} || '../lib/togo/admin'),'public')
7
+ })
@@ -0,0 +1,152 @@
1
+ require 'rack'
2
+ require 'erubis'
3
+
4
+ module Togo
5
+ class Dispatch
6
+
7
+ HANDLERS = %w(thin mongrel webrick)
8
+
9
+ attr_reader :request, :response, :params
10
+
11
+ def initialize(opts = {})
12
+ @view_path = opts[:view_path] || 'views'
13
+ ENV['RACK_ENV'] = (opts[:environment] || :development) if not ENV['RACK_ENV']
14
+ end
15
+
16
+ def symbolize_keys(hash)
17
+ hash.inject({}){|m,v| m.merge!(v[0].to_sym => v[1])}
18
+ end
19
+
20
+ def call(env)
21
+ dup.call!(env)
22
+ end
23
+
24
+ def call!(env)
25
+ @request = Rack::Request.new(env)
26
+ @response = Rack::Response.new
27
+
28
+ answer(@request.env['REQUEST_METHOD'], @request.path_info)
29
+ @response.finish
30
+ end
31
+
32
+ def answer(type,path)
33
+ method = nil
34
+ @params = symbolize_keys(@request.GET.dup.merge!(@request.POST.dup))
35
+ self.class.routes[type].each do |p,k,m|
36
+ if match = p.match(path)
37
+ method = m
38
+ @params.merge!(k.zip(match.captures.to_a).inject({}){|o,v| o.merge!(v[0].to_sym => v[1])}) unless k.empty?
39
+ break
40
+ end
41
+ end
42
+ if method.nil?
43
+ @response.status = 404
44
+ @response.write("404 Not Found")
45
+ else
46
+ begin
47
+ __before if defined? __before
48
+ @response.write(send(method))
49
+ rescue => detail
50
+ @response.status = 500
51
+ @response.write("Error: #{detail}")
52
+ end
53
+ end
54
+ end
55
+
56
+ def erb(content, opts = {}, &block)
57
+ if content.is_a?(Symbol)
58
+ content = File.open(File.join(@view_path,"#{content}.erb")).read
59
+ end
60
+ result = Erubis::Eruby.new(content).result(binding)
61
+ if not block_given? and opts[:layout] != false
62
+ result = erb(:layout){ result }
63
+ end
64
+ result
65
+ end
66
+
67
+ def redirect(location, opts = {})
68
+ @response.status = (opts[:status] || 301)
69
+ @response.headers['Location'] = location
70
+ @response.finish
71
+ end
72
+
73
+ def environment?(name)
74
+ ENV['RACK_ENV'] == name.to_sym
75
+ end
76
+
77
+ def self.handler
78
+ HANDLERS.each do |h|
79
+ begin
80
+ return Rack::Handler.get(h)
81
+ rescue
82
+ end
83
+ end
84
+ puts "Could not find any handlers to run. Please be sure your requested handler is installed."
85
+ end
86
+
87
+ class << self
88
+ attr_accessor :routes, :config
89
+
90
+ def inherited(subclass)
91
+ subclass.routes = {}
92
+ subclass.send(:include, Rack::Utils)
93
+ %w{GET POST}.each{|v| subclass.routes[v] = []}
94
+ subclass.config = {
95
+ :public_path => 'public',
96
+ :static_urls => ['/css','/js','/img'],
97
+ :port => 8080,
98
+ :host => '127.0.0.1',
99
+ :environment => :development
100
+ }
101
+ end
102
+
103
+ def get(route, &block)
104
+ answer('GET', route, &block)
105
+ end
106
+
107
+ def post(route, &block)
108
+ answer('POST', route, &block)
109
+ end
110
+
111
+ def answer(type, route, &block)
112
+ method_name = "__#{type.downcase}#{clean_path(route)}"
113
+ k = []
114
+ p = route.gsub(/(:\w+)/){|m| k << m[1..-1]; "([^?/#&]+)"}
115
+ routes[type].push([/^#{p}$/,k,method_name])
116
+ define_method(method_name, &block)
117
+ end
118
+
119
+ def before(&block)
120
+ define_method("__before",&block)
121
+ end
122
+
123
+ def clean_path(path)
124
+ path.gsub(/\/|\./, '__')
125
+ end
126
+
127
+ def configure(opts = {})
128
+ config.merge!(opts)
129
+ end
130
+
131
+ def run!(opts = {})
132
+ opts = config.dup.merge!(opts)
133
+ builder = Rack::Builder.new
134
+ if opts[:environment].to_sym == :development
135
+ puts "Showing exceptions and using reloader for Development..."
136
+ builder.use Rack::ShowExceptions
137
+ builder.use opts[:reloader] if opts[:reloader]
138
+ end
139
+ builder.use Rack::Static, :urls => opts[:static_urls], :root => opts[:public_path]
140
+ builder.run new(opts)
141
+ if opts[:standalone]
142
+ opts[:handler].run(builder.to_app, :Port => opts[:port], :Host => opts[:host])
143
+ else
144
+ builder.to_app
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ end # Dispatch
151
+
152
+ end # Togo
@@ -0,0 +1 @@
1
+ require 'togo/dispatch/dispatch'
@@ -0,0 +1,117 @@
1
+ require 'erubis/tiny'
2
+
3
+ module Togo
4
+ module DataMapper
5
+ module Model
6
+ BLACKLIST = [:id, :position]
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ base.send(:class_variable_set, :@@list_properties, [])
11
+ base.send(:class_variable_set, :@@form_properties, [])
12
+ base.send(:class_variable_set, :@@custom_form_templates, {})
13
+ base.send(:class_variable_set, :@@property_options, {})
14
+ if MODELS.include?(base) # support code reloading
15
+ MODELS[MODELS.index(base)] = base # preserve order of which models were loaded
16
+ else
17
+ MODELS << base
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ # Let the user determine what properties to show in list view
24
+ def list_properties(*args)
25
+ pick_properties(:list,*args)
26
+ end
27
+
28
+ # Let the user determine what properties to show in form view
29
+ def form_properties(*args)
30
+ pick_properties(:form,*args)
31
+ end
32
+
33
+ def configure_property(property,opts = {})
34
+ custom_template_for(property, opts.delete(:template)) if opts.has_key?(:template)
35
+ class_variable_get(:@@property_options).merge!(property => opts)
36
+ end
37
+
38
+ # Display the form template for a property
39
+ def form_for(property,content)
40
+ template = class_variable_get(:@@custom_form_templates)[property.name] || File.join(File.dirname(__FILE__),'types',"#{type_from_property(property)}.erb")
41
+ Erubis::TinyEruby.new(File.open(template).read).result(binding)
42
+ end
43
+
44
+ def update_content!(id,attrs)
45
+ stage_content(get(id),attrs).save!
46
+ end
47
+
48
+ def create_content!(attrs)
49
+ stage_content(new,attrs).save!
50
+ end
51
+
52
+ def stage_content(content,attrs)
53
+ content.attributes = properties.inject({}){|m,p| attrs[p.name.to_sym] ? m.merge!(p.name.to_sym => attrs[p.name.to_sym]) : m}
54
+ content
55
+ end
56
+
57
+ def delete_content(content)
58
+ content.destroy!
59
+ end
60
+
61
+ def display_name
62
+ name.gsub(/([a-z])([A-Z])/,"\\1 \\2").pluralize
63
+ end
64
+
65
+ def property_options
66
+ class_variable_get(:@@property_options)
67
+ end
68
+
69
+ def search(opts)
70
+ q = "%#{opts[:q].gsub(/\s+/,'%')}%"
71
+ conditions, values = [], []
72
+ search_properties.each{|l|
73
+ conditions << "#{l.name} like ?"
74
+ values << q
75
+ }
76
+ all(:conditions => [conditions.join(' OR ')] + values)
77
+ end
78
+
79
+ private
80
+
81
+ def custom_template_for(property,template)
82
+ class_variable_get(:@@custom_form_templates)[property] = template if File.exists?(template)
83
+ end
84
+
85
+ def type_from_property(property)
86
+ case property
87
+ when ::DataMapper::Property
88
+ Extlib::Inflection.demodulize(property.type).downcase
89
+ when ::DataMapper::Associations::ManyToOne::Relationship
90
+ 'belongs_to'
91
+ else
92
+ 'string'
93
+ end
94
+ end
95
+
96
+ def pick_properties(selection,*args)
97
+ if class_variable_get(:"@@#{selection}_properties").empty?
98
+ args = shown_properties.map{|p| p.name} if args.empty?
99
+ class_variable_set(:"@@#{selection}_properties", args.collect{|a| shown_properties.select{|s| s.name == a}.first}.compact)
100
+ end
101
+ class_variable_get(:"@@#{selection}_properties")
102
+ end
103
+
104
+ def shown_properties
105
+ properties.select{|p| not BLACKLIST.include?(p.name) and not p.name =~ /_id$/} + relationships.values
106
+ end
107
+
108
+ def search_properties
109
+ only_properties = [String, ::DataMapper::Types::Text]
110
+ properties.select{|p| only_properties.include?(p.type)}
111
+ end
112
+
113
+ end
114
+
115
+ end # Model
116
+ end # DataMapper
117
+ end # Togo
@@ -0,0 +1,3 @@
1
+ <label for="<%= property.name %>"><%= property.name.to_s.humanize.titleize %></label>
2
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
3
+ <input type="hidden" name="<%= property.child_key.first.name %>" id="<%= property.child_key.first.name %>" value="<%= content.send(property.name.to_sym).id rescue '' %>" />
@@ -0,0 +1,3 @@
1
+ <label><%= property.name.to_s.humanize.titleize %></label>
2
+ <input type="radio" name="<%= property.name %>" id="<%= property.name %>_true" value="1" <% if content.send(property.name.to_sym) %> checked="checked"<% end %>/> <label class="inline" for="<%= property.name %>_true">Yes</label>
3
+ <input type="radio" name="<%= property.name %>" id="<%= property.name %>_false" value="0" <% if not content.send(property.name.to_sym) %> checked="checked"<% end %>/> <label class="inline" for="<%= property.name %>_false">No</label>
@@ -0,0 +1,2 @@
1
+ <label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
2
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
@@ -0,0 +1,2 @@
1
+ <label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
2
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
@@ -0,0 +1,2 @@
1
+ <label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
2
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
@@ -0,0 +1,2 @@
1
+ <label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
2
+ <input type="text" name="<%= property.name %>" id="<%= property.name %>" value="<%= content.send(property.name.to_sym) %>" />
@@ -0,0 +1,2 @@
1
+ <label for="<%= property.name %>"><%= property.model.property_options[property.name][:label] rescue property.name.to_s.humanize.titleize %></label>
2
+ <textarea name="<%= property.name %>" id="<%= property.name %>" rows="10" cols="50"><%= content.send(property.name.to_sym) %></textarea>
data/lib/togo/model.rb ADDED
@@ -0,0 +1 @@
1
+ require 'togo/model/model'
@@ -0,0 +1,11 @@
1
+ class String
2
+
3
+ def titleize
4
+ self.gsub(/\b\w/){$&.upcase}
5
+ end
6
+
7
+ def humanize
8
+ self.tr('_',' ')
9
+ end
10
+
11
+ end
data/lib/togo.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Togo
2
+
3
+ MODELS = []
4
+
5
+ def self.models
6
+ MODELS
7
+ end
8
+
9
+ end
10
+
11
+ require 'togo/model'
12
+ require 'togo/support'
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: togo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt King
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-03 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.10.2
24
+ version:
25
+ description: With a few lines of code in your Ruby ORMs, you get a highly configurable and extensive content administration tool.
26
+ email: matt@mattking.org
27
+ executables:
28
+ - togo-admin
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - Changelog
36
+ - LICENSE
37
+ - lib/togo/admin/admin.rb
38
+ - lib/togo/admin/public/css/screen.css
39
+ - lib/togo/admin/public/img/bg-header.png
40
+ - lib/togo/admin/public/img/bg-headline.png
41
+ - lib/togo/admin/public/img/bg-nav.png
42
+ - lib/togo/admin/public/img/bg-side.png
43
+ - lib/togo/admin/public/img/btn-bg.gif
44
+ - lib/togo/admin/public/img/btn-bg.png
45
+ - lib/togo/admin/public/js/index.js
46
+ - lib/togo/admin/public/js/togo.js
47
+ - lib/togo/admin/views/custom_title.erb
48
+ - lib/togo/admin/views/edit.erb
49
+ - lib/togo/admin/views/index.erb
50
+ - lib/togo/admin/views/layout.erb
51
+ - lib/togo/admin/views/new.erb
52
+ - lib/togo/admin.rb
53
+ - lib/togo/dispatch/dispatch.rb
54
+ - lib/togo/dispatch.rb
55
+ - lib/togo/model/model.rb
56
+ - lib/togo/model/types/belongs_to.erb
57
+ - lib/togo/model/types/boolean.erb
58
+ - lib/togo/model/types/datetime.erb
59
+ - lib/togo/model/types/float.erb
60
+ - lib/togo/model/types/integer.erb
61
+ - lib/togo/model/types/string.erb
62
+ - lib/togo/model/types/text.erb
63
+ - lib/togo/model.rb
64
+ - lib/togo/support.rb
65
+ - lib/togo.rb
66
+ - bin/togo-admin
67
+ has_rdoc: true
68
+ homepage: http://github.com/mattking17/Togo/
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options: []
73
+
74
+ require_paths:
75
+ - lib
76
+ - lib/togo/model
77
+ - lib/togo/dispatch
78
+ - lib/togo/admin
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ version:
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.5
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Automatic Content Admin Tool for Ruby ORMs
98
+ test_files: []
99
+