camping 1.5.180 → 2.0.rc0
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 +35 -0
- data/README +43 -68
- data/Rakefile +155 -86
- data/bin/camping +64 -246
- data/book/01_introduction +19 -0
- data/book/02_getting_started +443 -0
- data/book/51_upgrading +93 -0
- data/doc/api.html +1953 -0
- data/doc/book.html +73 -0
- data/doc/book/01_introduction.html +57 -0
- data/doc/book/02_getting_started.html +573 -0
- data/doc/book/51_upgrading.html +146 -0
- data/doc/created.rid +1 -0
- data/{extras → doc/images}/Camping.gif +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/{extras → doc/images}/permalink.gif +0 -0
- data/doc/index.html +148 -0
- data/doc/js/camping.js +79 -0
- data/doc/js/jquery.js +32 -0
- data/doc/rdoc.css +117 -0
- data/examples/blog.rb +280 -181
- data/extras/images/badge.gif +0 -0
- data/extras/images/boys-life.png +0 -0
- data/extras/images/deerputer.png +0 -0
- data/extras/images/diagram.png +0 -0
- data/extras/images/hill.png +0 -0
- data/extras/images/i-wish.png +0 -0
- data/extras/images/latl.png +0 -0
- data/extras/images/little-wheels.png +0 -0
- data/extras/images/square-badge.png +0 -0
- data/extras/images/uniform.png +0 -0
- data/extras/images/whale-bounce.png +0 -0
- data/extras/rdoc/generator/singledarkfish.rb +205 -0
- data/extras/rdoc/generator/template/flipbook/images/Camping.gif +0 -0
- data/extras/rdoc/generator/template/flipbook/images/loadingAnimation.gif +0 -0
- data/extras/rdoc/generator/template/flipbook/images/permalink.gif +0 -0
- data/extras/rdoc/generator/template/flipbook/js/camping.js +79 -0
- data/extras/rdoc/generator/template/flipbook/js/jquery.js +32 -0
- data/extras/rdoc/generator/template/flipbook/page.rhtml +30 -0
- data/extras/rdoc/generator/template/flipbook/rdoc.css +117 -0
- data/extras/rdoc/generator/template/flipbook/readme.rhtml +31 -0
- data/extras/rdoc/generator/template/flipbook/reference.rhtml +71 -0
- data/extras/rdoc/generator/template/flipbook/toc.rhtml +43 -0
- data/lib/camping-unabridged.rb +420 -481
- data/lib/camping.rb +40 -55
- data/lib/camping/{db.rb → ar.rb} +5 -8
- data/lib/camping/mab.rb +26 -0
- data/lib/camping/reloader.rb +175 -147
- data/lib/camping/server.rb +178 -0
- data/lib/camping/session.rb +34 -121
- data/test/apps/env_debug.rb +65 -0
- data/test/apps/forms.rb +95 -0
- data/test/apps/forward_to_other_controller.rb +60 -0
- data/test/apps/migrations.rb +97 -0
- data/test/apps/misc.rb +86 -0
- data/test/apps/sessions.rb +38 -0
- metadata +120 -80
- data/doc/camping.1.gz +0 -0
- data/examples/campsh.rb +0 -630
- data/examples/tepee.rb +0 -242
- data/extras/flipbook_rdoc.rb +0 -491
- data/lib/camping/fastcgi.rb +0 -244
- data/lib/camping/webrick.rb +0 -65
- data/test/test_xhtml_trans.rb +0 -55
data/bin/camping
CHANGED
@@ -1,279 +1,97 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
trap("INT") { exit }
|
4
4
|
require 'optparse'
|
5
5
|
require 'ostruct'
|
6
|
-
|
7
6
|
require 'stringio'
|
8
|
-
require 'camping'
|
9
|
-
require 'camping/reloader'
|
10
7
|
require 'yaml'
|
11
8
|
|
9
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
10
|
+
require 'camping'
|
11
|
+
require 'camping/server'
|
12
|
+
|
12
13
|
conf = OpenStruct.new(:host => '0.0.0.0', :port => 3301)
|
13
14
|
|
14
15
|
# Setup paths
|
15
16
|
if home = ENV['HOME'] # POSIX
|
16
|
-
|
17
|
-
|
17
|
+
db_path = File.join(home, '.camping.db')
|
18
|
+
rc_path = File.join(home, '.campingrc')
|
18
19
|
elsif home = ENV['APPDATA'] # MSWIN
|
19
|
-
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
|
-
# Load configuration if any
|
24
|
-
if conf.rc and File.exists?( conf.rc )
|
25
|
-
YAML.load_file(conf.rc).each do |k,v|
|
26
|
-
conf.send("#{k}=", v)
|
27
|
-
end
|
20
|
+
db_path = File.join(home, 'Camping.db')
|
21
|
+
rc_path = File.join(home, 'Campingrc')
|
28
22
|
end
|
29
23
|
|
30
24
|
# Parse options
|
31
25
|
opts = OptionParser.new do |opts|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
puts opts
|
51
|
-
exit
|
52
|
-
end
|
53
|
-
|
54
|
-
# Another typical switch to print the version.
|
55
|
-
opts.on_tail("-v", "--version", "Show version") do
|
56
|
-
class << Gem; attr_accessor :loaded_specs; end
|
57
|
-
puts Gem.loaded_specs['camping'].version
|
58
|
-
exit
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
opts.parse! ARGV
|
63
|
-
if ARGV.length < 1
|
26
|
+
opts.banner = "Usage: camping app1.rb, app2.rb..."
|
27
|
+
opts.define_head "#{File.basename($0)}, the microframework ON-button for ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "Specific options:"
|
30
|
+
|
31
|
+
opts.on("-h", "--host HOSTNAME", "Host for web server to bind to (default is all IPs)") { |val| conf.host = val }
|
32
|
+
opts.on("-p", "--port NUM", "Port for web server (defaults to #{conf.port})") { |val| conf.port = val }
|
33
|
+
opts.on("-d", "--database FILE", "SQLite3 database path (defaults to #{db_path ? db_path : '<none>'})") { |db_path| conf.database = {:adapter => 'sqlite3', :database => db_path} }
|
34
|
+
opts.on("-C", "--console", "Run in console mode with IRB") { conf.server = "console" }
|
35
|
+
server_list = ["mongrel", "webrick", "console"]
|
36
|
+
opts.on("-s", "--server NAME", server_list, "Server to force (#{server_list.join(', ')})") { |val| conf.server = val }
|
37
|
+
|
38
|
+
opts.separator ""
|
39
|
+
opts.separator "Common options:"
|
40
|
+
|
41
|
+
# No argument, shows at tail. This will print an options summary.
|
42
|
+
# Try it and see!
|
43
|
+
opts.on_tail("-?", "--help", "Show this message") do
|
64
44
|
puts opts
|
65
45
|
exit
|
66
|
-
end
|
46
|
+
end
|
67
47
|
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
48
|
+
# Another typical switch to print the version.
|
49
|
+
opts.on_tail("-v", "--version", "Show version") do
|
50
|
+
puts Gem.loaded_specs['camping'].version
|
51
|
+
exit
|
72
52
|
end
|
73
|
-
conf.database = {:adapter => 'sqlite3', :database => conf.db}
|
74
53
|
end
|
75
54
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
apps << script
|
83
|
-
end
|
55
|
+
begin
|
56
|
+
opts.parse! ARGV
|
57
|
+
rescue OptionParser::ParseError => ex
|
58
|
+
STDERR.puts "!! #{ex.message}"
|
59
|
+
puts "** use `#{File.basename($0)} --help` for more details..."
|
60
|
+
exit 1
|
84
61
|
end
|
85
62
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
abort("** No apps successfully loaded") unless apps.detect { |app| app.klass }
|
90
|
-
|
91
|
-
def apps.find_new_scripts
|
92
|
-
each { |app| app.reload_app }
|
93
|
-
PATHS.each do |path|
|
94
|
-
Dir[File.join(path, '*.rb')].each do |script|
|
95
|
-
smount = File.basename(script, '.rb')
|
96
|
-
next if detect { |x| x.mount == smount }
|
97
|
-
|
98
|
-
puts "** Discovered new #{script}"
|
99
|
-
app = Camping::Reloader.new(script)
|
100
|
-
next unless app
|
101
|
-
|
102
|
-
yield app
|
103
|
-
self << app
|
104
|
-
end
|
105
|
-
end
|
106
|
-
self.sort! { |x, y| x.mount <=> y.mount }
|
63
|
+
if ARGV.length < 1
|
64
|
+
puts opts
|
65
|
+
exit 1
|
107
66
|
end
|
108
67
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
head do
|
116
|
-
title welcome
|
117
|
-
style <<-END, :type => 'text/css'
|
118
|
-
body {
|
119
|
-
font-family: verdana, arial, sans-serif;
|
120
|
-
padding: 10px 40px;
|
121
|
-
margin: 0;
|
122
|
-
}
|
123
|
-
h1, h2, h3, h4, h5, h6 {
|
124
|
-
font-family: utopia, georgia, serif;
|
125
|
-
}
|
126
|
-
END
|
127
|
-
end
|
128
|
-
body do
|
129
|
-
h1 welcome
|
130
|
-
p %{Good day. These are the Camping apps you've mounted.}
|
131
|
-
ul do
|
132
|
-
apps.each do |app|
|
133
|
-
next unless app.klass
|
134
|
-
li do
|
135
|
-
h3(:style => "display: inline") { a app.klass.name, :href => "/#{app.mount}" }
|
136
|
-
small { text " / " ; a "View Source", :href => "/code/#{app.mount}" }
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
b.to_s
|
68
|
+
# Load configuration if any
|
69
|
+
if rc_path and File.exists?(rc_path)
|
70
|
+
YAML.load_file(rc_path).each do |k,v|
|
71
|
+
conf.send("#{k}=", v) unless conf.send(k)
|
72
|
+
end
|
73
|
+
puts "** conf file #{rc_path} loaded"
|
144
74
|
end
|
145
75
|
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
require 'mongrel'
|
150
|
-
require 'mongrel/camping'
|
151
|
-
conf.server = :mongrel
|
152
|
-
rescue LoadError
|
153
|
-
conf.server = :webrick
|
154
|
-
end
|
76
|
+
# Default db
|
77
|
+
if conf.database.nil? and db_path
|
78
|
+
conf.database = { :adapter => 'sqlite3', :database => db_path }
|
155
79
|
end
|
156
80
|
|
157
|
-
# Running the selected server
|
158
|
-
case conf.server.to_s
|
159
|
-
when 'mongrel'
|
160
|
-
require 'mongrel'
|
161
|
-
require 'mongrel/camping'
|
162
|
-
|
163
|
-
class IndexHandler < Mongrel::HttpHandler
|
164
|
-
def initialize(apps, server)
|
165
|
-
@apps = apps
|
166
|
-
@server = server
|
167
|
-
end
|
168
|
-
def process(req, res)
|
169
|
-
res.start(200) do |head, out|
|
170
|
-
@apps.find_new_scripts do |app|
|
171
|
-
@server.register "/#{app.mount}", Mongrel::Camping::CampingHandler.new(app)
|
172
|
-
@server.register "/code/#{app.mount}", ViewSource.new(app)
|
173
|
-
end
|
174
|
-
out << @apps.index_page
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
class ViewSource < Mongrel::HttpHandler
|
179
|
-
def initialize(app)
|
180
|
-
@app = app
|
181
|
-
end
|
182
|
-
def process(req, res)
|
183
|
-
res.start(200) do |head, out|
|
184
|
-
head['Content-Type'] = 'text/plain'
|
185
|
-
out << @app.view_source
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
begin
|
190
|
-
config = Mongrel::Configurator.new :host => conf.host do
|
191
|
-
listener :port => conf.port do
|
192
|
-
if apps.length > 1
|
193
|
-
apps.each do |app|
|
194
|
-
uri "/#{app.mount}", :handler => Mongrel::Camping::CampingHandler.new(app)
|
195
|
-
uri "/code/#{app.mount}", :handler => ViewSource.new(app)
|
196
|
-
end
|
197
|
-
uri "/", :handler => IndexHandler.new(apps, @listener)
|
198
|
-
else
|
199
|
-
uri "/", :handler => Mongrel::Camping::CampingHandler.new(apps.first)
|
200
|
-
end
|
201
|
-
uri "/favicon.ico", :handler => Mongrel::Error404Handler.new("")
|
202
|
-
trap("INT") { stop }
|
203
|
-
run
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
puts "** Camping running on #{conf.host}:#{conf.port}."
|
208
|
-
config.join
|
209
|
-
rescue Errno::EADDRINUSE
|
210
|
-
puts "** ERROR : address #{conf.host}:#{conf.port} already in use."
|
211
|
-
end
|
212
|
-
when 'webrick'
|
213
|
-
require 'webrick/httpserver'
|
214
|
-
require 'camping/webrick'
|
215
|
-
|
216
|
-
# Mount the root
|
217
|
-
s = WEBrick::HTTPServer.new(:BindAddress => conf.host, :Port => conf.port)
|
218
|
-
if apps.length > 1
|
219
|
-
apps.each do |app|
|
220
|
-
s.mount "/#{app.mount}", WEBrick::CampingHandler, app
|
221
|
-
s.mount_proc("/code/#{app.mount}") do |req, resp|
|
222
|
-
resp['Content-Type'] = 'text/plain'
|
223
|
-
resp.body = app.view_source
|
224
|
-
end
|
225
|
-
end
|
226
|
-
s.mount_proc("/") do |req, resp|
|
227
|
-
apps.find_new_scripts do |app|
|
228
|
-
s.mount "/#{app.mount}", WEBrick::CampingHandler, app
|
229
|
-
s.mount_proc("/code/#{app.mount}") do |req, resp|
|
230
|
-
resp['Content-Type'] = 'text/plain'
|
231
|
-
resp.body = app.view_source
|
232
|
-
end
|
233
|
-
end
|
234
|
-
resp.body = apps.index_page
|
235
|
-
end
|
236
|
-
else
|
237
|
-
s.mount "/", WEBrick::CampingHandler, apps.first
|
238
|
-
end
|
239
|
-
|
240
|
-
# Server up
|
241
|
-
trap(:INT) do
|
242
|
-
s.shutdown
|
243
|
-
end
|
244
|
-
s.start
|
245
|
-
when 'lighttpd'
|
246
|
-
require 'rbconfig'
|
247
|
-
ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['RUBY_INSTALL_NAME'])
|
248
|
-
dispatcher =<<SCRIPT
|
249
|
-
#!#{ruby}
|
250
|
-
require 'rubygems'
|
251
|
-
require 'camping/fastcgi'
|
252
|
-
Camping::Models::Base.establish_connection(Marshal.load(#{Marshal.dump(conf.database).dump}))
|
253
|
-
Camping::FastCGI.serve("")
|
254
|
-
SCRIPT
|
255
|
-
lighttpd_conf =<<CONF
|
256
|
-
server.port = #{conf.port}
|
257
|
-
server.bind = "#{conf.host}"
|
258
|
-
server.modules = ( "mod_fastcgi" )
|
259
|
-
server.document-root = "/dont/need/one"
|
260
81
|
|
261
|
-
|
262
|
-
|
263
|
-
"localhost" => (
|
264
|
-
"socket" => "/tmp/camping-blog.socket",
|
265
|
-
"bin-path" => "#{conf.dispatcher}",
|
266
|
-
"check-local" => "disable",
|
267
|
-
"max-procs" => 1 ) ) )
|
268
|
-
CONF
|
269
|
-
|
270
|
-
when 'console'
|
271
|
-
ARGV.clear # Avoid passing args to IRB
|
82
|
+
# get a copy of the paths to pass to the server
|
83
|
+
paths = ARGV.dup
|
272
84
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
85
|
+
# Check that mongrel exists
|
86
|
+
if conf.server.nil? || conf.server == "mongrel"
|
87
|
+
begin
|
88
|
+
require 'mongrel'
|
89
|
+
conf.server = "mongrel"
|
90
|
+
rescue LoadError
|
91
|
+
puts "!! Could not load mongrel. Falling back to webrick."
|
92
|
+
conf.server = "webrick"
|
93
|
+
end
|
279
94
|
end
|
95
|
+
|
96
|
+
server = Camping::Server.new(conf, paths)
|
97
|
+
server.start
|
@@ -0,0 +1,19 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
Camping is a small web framework, less than 4k, a little white blood cell in
|
4
|
+
the vein of Rails. This little book will start with a tutorial which takes
|
5
|
+
about fifteen minutes - by the end you should have a little Camping site up.
|
6
|
+
The following chapters will eventually go deeper into how both Camping, HTTP
|
7
|
+
and Rack works.
|
8
|
+
|
9
|
+
("Eventually", because these chapters are not written yet. This book is
|
10
|
+
currently a very much work in progress, and we'll be very grateful if you want
|
11
|
+
to help out.)
|
12
|
+
|
13
|
+
If you at any moment need some help or have any questions or comments, we
|
14
|
+
highly recommend {the mailing list}[http://rubyforge.org/mailman/listinfo/camping-list]
|
15
|
+
which got plenty of nice people willing to help. We also have an IRC-channel
|
16
|
+
at {#camping @ irc.freenode.net}[http://java.freenode.net/?channel=camping]
|
17
|
+
if you're into that sort of things.
|
18
|
+
|
19
|
+
Enough talk. Ready? Let's {"get started"}[link:book/02_getting_started.html].
|
@@ -0,0 +1,443 @@
|
|
1
|
+
= Getting Started
|
2
|
+
|
3
|
+
Start a new text file called nuts.rb. Here's what you put inside:
|
4
|
+
|
5
|
+
Camping.goes :Nuts
|
6
|
+
|
7
|
+
Save it. Then, open a command prompt in the same directory. You'll want to
|
8
|
+
run:
|
9
|
+
|
10
|
+
$ camping nuts.rb
|
11
|
+
|
12
|
+
And you should get a message which reads:
|
13
|
+
|
14
|
+
** Camping running on 0.0.0.0:3301.
|
15
|
+
|
16
|
+
This means that right now The Camping Server is running on port 3301 on your
|
17
|
+
machine. Open your browser and visit http://localhost:3301/.
|
18
|
+
|
19
|
+
Your browser window should show:
|
20
|
+
|
21
|
+
Camping Problem!
|
22
|
+
|
23
|
+
/ Not found
|
24
|
+
|
25
|
+
No problem with that. The Camping Server is running, but it doesn't know what
|
26
|
+
to show. Let's tell him.
|
27
|
+
|
28
|
+
== Hello clock
|
29
|
+
|
30
|
+
So, you've got Camping installed and it's running. Keep it running. You can
|
31
|
+
edit files and The Camping Server will reload automatically. When you need to
|
32
|
+
stop the server, press Control-C.
|
33
|
+
|
34
|
+
Let's show something. At the bottom of nuts.rb add:
|
35
|
+
|
36
|
+
module Nuts::Controllers
|
37
|
+
class Index < R '/'
|
38
|
+
def get
|
39
|
+
Time.now.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Save the file and refresh the browser window. Your browser window should show
|
45
|
+
the time, e.g.
|
46
|
+
|
47
|
+
Sun Jul 15 12:56:15 +0200 2007
|
48
|
+
|
49
|
+
== Enjoying the view
|
50
|
+
|
51
|
+
The Camping microframework allows us to separate our code using the MVC
|
52
|
+
(Model-View-Controller) design pattern. Let's add a view to our Nuts
|
53
|
+
application. Replace the <tt>module Nuts::Controllers</tt> with:
|
54
|
+
|
55
|
+
module Nuts::Controllers
|
56
|
+
class Index < R '/'
|
57
|
+
def get
|
58
|
+
@time = Time.now
|
59
|
+
render :sundial
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Nuts::Views
|
65
|
+
def layout
|
66
|
+
html do
|
67
|
+
head do
|
68
|
+
title { "Nuts And GORP" }
|
69
|
+
end
|
70
|
+
body { self << yield }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def sundial
|
75
|
+
p "The current time is: #{@time}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Save the file and refresh your browser window and it should show a message
|
80
|
+
like:
|
81
|
+
|
82
|
+
The current time is: Sun Jul 15 13:05:41 +0200 2007
|
83
|
+
|
84
|
+
And the window title reads "Nuts And GORP".
|
85
|
+
|
86
|
+
Here you can see we call <tt>render :sundial</tt> from our controller. This
|
87
|
+
does exactly what it says, and renders our <tt>sundial</tt> method. We've also
|
88
|
+
added a special method called <tt>layout</tt> which Camping will automatically
|
89
|
+
wrap our sundial output in. If you're familiar with HTML, you'll see that our
|
90
|
+
view contains what looks HTML tag names. This is Markaby, which is like
|
91
|
+
writing HTML using Ruby!
|
92
|
+
|
93
|
+
Soon enough, you'll find that you can return anything from the controller, and
|
94
|
+
it will be sent to the browser. But let's keep that for later and start
|
95
|
+
investigating the routes.
|
96
|
+
|
97
|
+
== Routes
|
98
|
+
|
99
|
+
You probably noticed the weird <tt>R '/'</tt> syntax in the previous page.
|
100
|
+
This is an uncommon feature of Ruby that is used in our favorite
|
101
|
+
microframework, to describe the routes which the controller can be accessed
|
102
|
+
on.
|
103
|
+
|
104
|
+
These routes can be very powerful, but we're going to have look at the
|
105
|
+
simplest ones first.
|
106
|
+
|
107
|
+
module Nuts::Controllers
|
108
|
+
class Words < R '/welcome/to/my/site'
|
109
|
+
def get
|
110
|
+
"You got here by: /welcome/to/my/site"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Digits < R '/nuts/(\d+)'
|
115
|
+
def get(number)
|
116
|
+
"You got here by: /nuts/#{number}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Segment < R '/gorp/([^/]+)'
|
121
|
+
def get(everything_else_than_a_slash)
|
122
|
+
"You got here by: /gorp/#{everything_else_than_a_slash}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class DigitsAndEverything < R '/nuts/(\d+)/([^/]+)'
|
127
|
+
def get(number, everything)
|
128
|
+
"You got here by: /nuts/#{number}/#{everything}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
Add this to nuts.rb and try if you can hit all of the controllers.
|
134
|
+
|
135
|
+
Also notice how everything inside a parenthesis gets passed into the method,
|
136
|
+
and is ready at your disposal.
|
137
|
+
|
138
|
+
=== Simpler routes
|
139
|
+
|
140
|
+
This just in:
|
141
|
+
|
142
|
+
module Nuts::Controllers
|
143
|
+
class Index
|
144
|
+
def get
|
145
|
+
"You got here by: /"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class WelcomeToMySite
|
150
|
+
def get
|
151
|
+
"You got here by: /welcome/to/my/site"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class NutsN
|
156
|
+
def get(number)
|
157
|
+
"You got here by: /nuts/#{number}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class GorpX
|
162
|
+
def get(everything_else_than_a_slash)
|
163
|
+
"You got here by: /gorp/#{everything_else_than_a_slash}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class NutsNX
|
168
|
+
def get(number, everything)
|
169
|
+
"You got here by: /nuts/#{number}/#{everything}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
Drop the <tt>< R</tt>-part and it attemps to read your mind. It won't always
|
175
|
+
succeed, but it can simplify your application once in a while.
|
176
|
+
|
177
|
+
== Modeling the world
|
178
|
+
|
179
|
+
You can get pretty far with what you've learned now, and hopefully you've been
|
180
|
+
playing a bit off-book, but it's time to take the next step: Storing data.
|
181
|
+
|
182
|
+
Let's start over again.
|
183
|
+
|
184
|
+
Camping.goes :Nuts
|
185
|
+
|
186
|
+
module Nuts::Models
|
187
|
+
class Page < Base
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
Obviously, this won't do anything, since we don't have any controllers, but
|
192
|
+
let's rather have a look at we _do_ have.
|
193
|
+
|
194
|
+
We have a model named Page. This means we now can store wiki pages and
|
195
|
+
retrieve them later. In fact, we can have as many models as we want. Need one
|
196
|
+
for your users and one for your blog posts? Well, I think you already know how
|
197
|
+
to do it.
|
198
|
+
|
199
|
+
However, our model is missing something essential: a skeleton.
|
200
|
+
|
201
|
+
Camping.goes :Nuts
|
202
|
+
|
203
|
+
module Nuts::Models
|
204
|
+
class Page < Base
|
205
|
+
end
|
206
|
+
|
207
|
+
class BasicFields < V 1.0
|
208
|
+
def self.up
|
209
|
+
create_table Page.table_name do |t|
|
210
|
+
t.string :title
|
211
|
+
t.text :content
|
212
|
+
# This gives us created_at and updated_at
|
213
|
+
t.timestamps
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.down
|
218
|
+
drop_table Page.table_name
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
Now we have our first version of our model. It says:
|
224
|
+
|
225
|
+
If you want to migrate up to version one,
|
226
|
+
create the skeleton for the Page model,
|
227
|
+
which should be able to store,
|
228
|
+
"title" which is a string,
|
229
|
+
"content" which is a larger text,
|
230
|
+
"created_at" which is the time it was created,
|
231
|
+
"updated_at" which is the previous time it was updated.
|
232
|
+
|
233
|
+
If you want to migrate down from version one,
|
234
|
+
remove the skeleton for the Page model.
|
235
|
+
|
236
|
+
This is called a _migration_. Whenever you want to change or add new models you simply add a new migration below, where you increase the version number. All of these migrations builds upon each other like LEGO blocks.
|
237
|
+
|
238
|
+
Now we just need to tell Camping to use our migration. Write this at the bottom of nuts.rb
|
239
|
+
|
240
|
+
def Nuts.create
|
241
|
+
Nuts::Models.create_schema
|
242
|
+
end
|
243
|
+
|
244
|
+
When The Camping Server boots up, it will automatically call
|
245
|
+
<tt>Nuts.create</tt>. You can put all kind of startup-code here, but right now
|
246
|
+
we only want to create our skeleton (or upgrade if needed). Start The Camping
|
247
|
+
Server again and observe:
|
248
|
+
|
249
|
+
$ camping nuts.rb
|
250
|
+
** Starting Mongrel on 0.0.0.0:3301
|
251
|
+
-- create_table("nuts_schema_infos")
|
252
|
+
-> 0.1035s
|
253
|
+
== Nuts::Models::BasicFields: migrating ===================================
|
254
|
+
-- create_table(:nuts_pages)
|
255
|
+
-> 0.0033s
|
256
|
+
== Nuts::Models::BasicFields: migrated (0.0038s) ==========================
|
257
|
+
|
258
|
+
Restart it, and enjoy the silence. There's no point of re-creating the
|
259
|
+
skeleton this time.
|
260
|
+
|
261
|
+
Before we go on, there's one rule you must known: Always place your models
|
262
|
+
before your migrations.
|
263
|
+
|
264
|
+
== Using our model
|
265
|
+
|
266
|
+
Let's explore how our model works by going into the _console_
|
267
|
+
|
268
|
+
$ camping -C nuts.rb
|
269
|
+
** Starting console
|
270
|
+
>>
|
271
|
+
|
272
|
+
Now it's waiting for your input, and will give you the answer when you press
|
273
|
+
Enter. Here's what I did, leaving out the boring answers. You should add your
|
274
|
+
own pages.
|
275
|
+
|
276
|
+
>> Page = Nuts::Models::Page
|
277
|
+
|
278
|
+
>> hiking = Page.new(:title => "Hiking")
|
279
|
+
>> hiking.content = "You can also set the values like this."
|
280
|
+
>> hiking.save
|
281
|
+
|
282
|
+
>> page = Page.find_by_title("Hiking")
|
283
|
+
=> #<Nuts::Models::Page id: 1, ... >
|
284
|
+
>> page = Page.find(1)
|
285
|
+
=> #<Nuts::Models::Page id: 1, ... >
|
286
|
+
>> page.title
|
287
|
+
>> page.content
|
288
|
+
>> page.created_at
|
289
|
+
>> page.updated_at
|
290
|
+
|
291
|
+
>> Page.find_by_title("Fishing")
|
292
|
+
=> nil
|
293
|
+
|
294
|
+
## Page.create automatically saves the page for you.
|
295
|
+
>> Page.create(:title => "Fishing", :content => "Go fish!")
|
296
|
+
|
297
|
+
>> Page.count
|
298
|
+
=> 2
|
299
|
+
|
300
|
+
Now I have two pages: One about hiking and one about fishing.
|
301
|
+
|
302
|
+
== Wrapping it up
|
303
|
+
|
304
|
+
Wouldn't it be nice if we could show this wonderful our pages in a browser?
|
305
|
+
Update nuts.rb so it also contains something like this:
|
306
|
+
|
307
|
+
module Nuts::Controllers
|
308
|
+
class Pages
|
309
|
+
def get
|
310
|
+
# Only fetch the titles of the pages.
|
311
|
+
@pages = Page.all(:select => "title")
|
312
|
+
render :list
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
class PageX
|
317
|
+
def get(title)
|
318
|
+
@page = Page.find_by_title(title)
|
319
|
+
render :view
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
module Nuts::Views
|
325
|
+
def list
|
326
|
+
h1 "All pages"
|
327
|
+
ul do
|
328
|
+
@pages.each do |page|
|
329
|
+
li do
|
330
|
+
a page.title, :href => R(PageX, page.title)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def view
|
337
|
+
h1 @page.title
|
338
|
+
self << @page.content
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
Here we meet our first _helper_:
|
343
|
+
|
344
|
+
R(PageX, page.title)
|
345
|
+
|
346
|
+
This is the <em>reversed router</em> and it generates a URL based on a
|
347
|
+
controller. Camping ships with a few, but very useful, helpers and you can
|
348
|
+
easily add your owns. Have a look at Camping::Helpers for how you use these.
|
349
|
+
|
350
|
+
There's a lot of improvements you could do here. Let me suggest:
|
351
|
+
|
352
|
+
* Show when the page was created and last updated.
|
353
|
+
* What happens when the page doesn't exist?
|
354
|
+
* What should the front page show?
|
355
|
+
* Add a layout.
|
356
|
+
* Jazz it up a bit.
|
357
|
+
|
358
|
+
== The last touch
|
359
|
+
|
360
|
+
We have one major flaw in our little application. You can't edit or add new
|
361
|
+
pages. Let's see if we can fix that:
|
362
|
+
|
363
|
+
module Nuts::Controllers
|
364
|
+
class PageX
|
365
|
+
def get(title)
|
366
|
+
if @page = Page.find_by_title(title)
|
367
|
+
render :view
|
368
|
+
else
|
369
|
+
redirect PageXEdit, title
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def post(title)
|
374
|
+
# If it doesn't exist, initialize it:
|
375
|
+
@page = Page.find_or_initialize_by_title(title)
|
376
|
+
# This is the same as:
|
377
|
+
# @page = Page.find_by_title(title) || Page.new(:title => title)
|
378
|
+
|
379
|
+
@page.content = @input.content
|
380
|
+
@page.save
|
381
|
+
redirect PageX, title
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class PageXEdit
|
386
|
+
def get(title)
|
387
|
+
@page = Page.find_or_initialize_by_title(title)
|
388
|
+
render :edit
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
The core of this code lies in the new <tt>post</tt> method in the PageX
|
394
|
+
controller. When someone types an address or follows a link, they'll end up at
|
395
|
+
the <tt>get</tt> method, but you can easily create a form which rather sends
|
396
|
+
you to the <tt>post</tt> when submitted.
|
397
|
+
|
398
|
+
There are other names you can use, but they won't always work. So for now,
|
399
|
+
don't be fancy and just stick to <tt>get</tt> and <tt>post</tt>. We'll show
|
400
|
+
you how this really works later.
|
401
|
+
|
402
|
+
You might also notice that we use <tt>@input.content</tt>. The
|
403
|
+
<tt>@input</tt>-hash contains any extra parameters sent, like those in the
|
404
|
+
forms and those in the URL (<tt>/posts?page=50</tt>).
|
405
|
+
|
406
|
+
Here's an <tt>edit</tt>-view, but you can probably do better. See if you can
|
407
|
+
integrate all of this with what you already have.
|
408
|
+
|
409
|
+
module Nuts::Views
|
410
|
+
def edit
|
411
|
+
h1 @page.title
|
412
|
+
form :action => R(PageX, @page.title), :method => :post do
|
413
|
+
textarea @page.content, :name => :content,
|
414
|
+
:rows => 10, :cols => 50
|
415
|
+
|
416
|
+
br
|
417
|
+
|
418
|
+
input :type => :submit, :value => "Submit!"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
== Phew.
|
425
|
+
|
426
|
+
You've taken quite a few steps in the last minutes. You deserve a break. But
|
427
|
+
let's recap for a moment:
|
428
|
+
|
429
|
+
* Always place <tt>Camping.goes :App</tt> at the top of your file.
|
430
|
+
* Every route ends at a controller, but ...
|
431
|
+
* ... the controller only delegates the work.
|
432
|
+
* <tt>@input</tt> contains the extra parameters.
|
433
|
+
* The views are HTML disguised as Ruby.
|
434
|
+
* They can access the instances variables (those that starts with a single
|
435
|
+
at-sign) from the controller.
|
436
|
+
* The models allows you to store all kinds of data.
|
437
|
+
* Place your models before your migrations.
|
438
|
+
* Helpers are helpful.
|
439
|
+
|
440
|
+
Unfortunately, the book stops here for now. Come back in a few months, or join
|
441
|
+
the mailing list to stay updated, and hopefully there's another chapter
|
442
|
+
waiting for you.
|
443
|
+
|