togo 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +1 -0
- data/LICENSE +22 -0
- data/README +23 -0
- data/bin/togo-admin +46 -0
- data/lib/togo/admin/admin.rb +76 -0
- data/lib/togo/admin/public/css/screen.css +208 -0
- data/lib/togo/admin/public/img/bg-header.png +0 -0
- data/lib/togo/admin/public/img/bg-headline.png +0 -0
- data/lib/togo/admin/public/img/bg-nav.png +0 -0
- data/lib/togo/admin/public/img/bg-side.png +0 -0
- data/lib/togo/admin/public/img/btn-bg.gif +0 -0
- data/lib/togo/admin/public/img/btn-bg.png +0 -0
- data/lib/togo/admin/public/js/index.js +32 -0
- data/lib/togo/admin/public/js/togo.js +3 -0
- data/lib/togo/admin/views/custom_title.erb +3 -0
- data/lib/togo/admin/views/edit.erb +20 -0
- data/lib/togo/admin/views/index.erb +44 -0
- data/lib/togo/admin/views/layout.erb +24 -0
- data/lib/togo/admin/views/new.erb +14 -0
- data/lib/togo/admin.rb +7 -0
- data/lib/togo/dispatch/dispatch.rb +152 -0
- data/lib/togo/dispatch.rb +1 -0
- data/lib/togo/model/model.rb +117 -0
- data/lib/togo/model/types/belongs_to.erb +3 -0
- data/lib/togo/model/types/boolean.erb +3 -0
- data/lib/togo/model/types/datetime.erb +2 -0
- data/lib/togo/model/types/float.erb +2 -0
- data/lib/togo/model/types/integer.erb +2 -0
- data/lib/togo/model/types/string.erb +2 -0
- data/lib/togo/model/types/text.erb +2 -0
- data/lib/togo/model.rb +1 -0
- data/lib/togo/support.rb +11 -0
- data/lib/togo.rb +12 -0
- metadata +99 -0
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
|
+
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -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,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
|
+
<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'
|
data/lib/togo/support.rb
ADDED
data/lib/togo.rb
ADDED
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
|
+
|