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 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
+