togo 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|