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.
Files changed (64) hide show
  1. data/CHANGELOG +35 -0
  2. data/README +43 -68
  3. data/Rakefile +155 -86
  4. data/bin/camping +64 -246
  5. data/book/01_introduction +19 -0
  6. data/book/02_getting_started +443 -0
  7. data/book/51_upgrading +93 -0
  8. data/doc/api.html +1953 -0
  9. data/doc/book.html +73 -0
  10. data/doc/book/01_introduction.html +57 -0
  11. data/doc/book/02_getting_started.html +573 -0
  12. data/doc/book/51_upgrading.html +146 -0
  13. data/doc/created.rid +1 -0
  14. data/{extras → doc/images}/Camping.gif +0 -0
  15. data/doc/images/loadingAnimation.gif +0 -0
  16. data/{extras → doc/images}/permalink.gif +0 -0
  17. data/doc/index.html +148 -0
  18. data/doc/js/camping.js +79 -0
  19. data/doc/js/jquery.js +32 -0
  20. data/doc/rdoc.css +117 -0
  21. data/examples/blog.rb +280 -181
  22. data/extras/images/badge.gif +0 -0
  23. data/extras/images/boys-life.png +0 -0
  24. data/extras/images/deerputer.png +0 -0
  25. data/extras/images/diagram.png +0 -0
  26. data/extras/images/hill.png +0 -0
  27. data/extras/images/i-wish.png +0 -0
  28. data/extras/images/latl.png +0 -0
  29. data/extras/images/little-wheels.png +0 -0
  30. data/extras/images/square-badge.png +0 -0
  31. data/extras/images/uniform.png +0 -0
  32. data/extras/images/whale-bounce.png +0 -0
  33. data/extras/rdoc/generator/singledarkfish.rb +205 -0
  34. data/extras/rdoc/generator/template/flipbook/images/Camping.gif +0 -0
  35. data/extras/rdoc/generator/template/flipbook/images/loadingAnimation.gif +0 -0
  36. data/extras/rdoc/generator/template/flipbook/images/permalink.gif +0 -0
  37. data/extras/rdoc/generator/template/flipbook/js/camping.js +79 -0
  38. data/extras/rdoc/generator/template/flipbook/js/jquery.js +32 -0
  39. data/extras/rdoc/generator/template/flipbook/page.rhtml +30 -0
  40. data/extras/rdoc/generator/template/flipbook/rdoc.css +117 -0
  41. data/extras/rdoc/generator/template/flipbook/readme.rhtml +31 -0
  42. data/extras/rdoc/generator/template/flipbook/reference.rhtml +71 -0
  43. data/extras/rdoc/generator/template/flipbook/toc.rhtml +43 -0
  44. data/lib/camping-unabridged.rb +420 -481
  45. data/lib/camping.rb +40 -55
  46. data/lib/camping/{db.rb → ar.rb} +5 -8
  47. data/lib/camping/mab.rb +26 -0
  48. data/lib/camping/reloader.rb +175 -147
  49. data/lib/camping/server.rb +178 -0
  50. data/lib/camping/session.rb +34 -121
  51. data/test/apps/env_debug.rb +65 -0
  52. data/test/apps/forms.rb +95 -0
  53. data/test/apps/forward_to_other_controller.rb +60 -0
  54. data/test/apps/migrations.rb +97 -0
  55. data/test/apps/misc.rb +86 -0
  56. data/test/apps/sessions.rb +38 -0
  57. metadata +120 -80
  58. data/doc/camping.1.gz +0 -0
  59. data/examples/campsh.rb +0 -630
  60. data/examples/tepee.rb +0 -242
  61. data/extras/flipbook_rdoc.rb +0 -491
  62. data/lib/camping/fastcgi.rb +0 -244
  63. data/lib/camping/webrick.rb +0 -65
  64. 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
- require 'delegate'
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
- conf.db = File.join(home, '.camping.db')
17
- conf.rc = File.join(home, '.campingrc')
17
+ db_path = File.join(home, '.camping.db')
18
+ rc_path = File.join(home, '.campingrc')
18
19
  elsif home = ENV['APPDATA'] # MSWIN
19
- conf.db = File.join(home, 'Camping.db')
20
- conf.rc = File.join(home, 'Campingrc')
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
- opts.banner = "Usage: camping app1.rb, app2.rb..."
33
- opts.define_head "#{File.basename($0)}, the microframework ON-button for ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
34
- opts.separator ""
35
- opts.separator "Specific options:"
36
-
37
- opts.on("-h", "--host HOSTNAME", "Host for web server to bind to (default is all IPs)") { |conf.host| }
38
- opts.on("-p", "--port NUM", "Port for web server (defaults to #{conf.port})") { |conf.port| }
39
- opts.on("-d", "--database FILE", "Database file (defaults to #{conf.db})") { |conf.db| }
40
- opts.on("-l", "--log FILE", "Start a database log ('-' for STDOUT)") { |conf.log| }
41
- opts.on("-C", "--console", "Run in console mode with IRB") { conf.server = :console }
42
- opts.on("-s", "--server NAME", "Server to force (mongrel, webrick, console)") { |s| conf.server = s.to_sym }
43
-
44
- opts.separator ""
45
- opts.separator "Common options:"
46
-
47
- # No argument, shows at tail. This will print an options summary.
48
- # Try it and see!
49
- opts.on_tail("-?", "--help", "Show this message") do
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
- # Default database
69
- unless conf.database
70
- unless conf.db
71
- puts "!! No home directory found. Please specify a database file, see --help."; exit
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
- # Load apps
77
- PATHS = ARGV.dup
78
- apps = PATHS.inject([]) do |apps, script|
79
- if File.directory? script
80
- apps.push(*Dir[File.join(script, '*.rb')])
81
- else
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
- Camping::Reloader.database = conf.database
87
- Camping::Reloader.log = conf.log
88
- apps.map! { |script| Camping::Reloader.new(script) }
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
- def apps.index_page
110
- welcome = "You are Camping"
111
- apps = self
112
- b = Markaby::Builder.new({}, {})
113
- b = b.instance_eval do
114
- html do
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
- # Check that mongrel exists
147
- unless conf.server
148
- begin
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
- #### fastcgi module
262
- fastcgi.server = ( "/" => (
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
- require 'irb'
274
- require 'irb/completion'
275
- if File.exists? ".irbrc"
276
- ENV['IRBRC'] = ".irbrc"
277
- end
278
- IRB.start
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
+