camping 1.5.180 → 2.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
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
+