Agile Web Development with Rails, Edition 5
6.1 Iteration A1: Creating the Products Maintenance Application
Table of Contents
2 Instant Gratification
We start with a simple "hello world!" demo application and in the process verify that everything is installed correctly.
Create the application
bundle exec /home/rubys/git/rails/railties/exe/rails new demo1 --skip-bundle --skip-listen
create
create README.md
create Rakefile
create config.ru
create .gitignore
create Gemfile
run git init from "."
Initialized empty Git repository in /home/rubys/git/awdwr/edition4/work-240/demo1/.git/
create app
create app/assets/config/manifest.js
create app/assets/javascripts/application.js
create app/assets/javascripts/cable.js
create app/assets/stylesheets/application.css
create app/channels/application_cable/channel.rb
create app/channels/application_cable/connection.rb
create app/controllers/application_controller.rb
create app/helpers/application_helper.rb
create app/jobs/application_job.rb
create app/mailers/application_mailer.rb
create app/models/application_record.rb
create app/views/layouts/application.html.erb
create app/views/layouts/mailer.html.erb
create app/views/layouts/mailer.text.erb
create app/assets/images/.keep
create app/assets/javascripts/channels
create app/assets/javascripts/channels/.keep
create app/controllers/concerns/.keep
create app/models/concerns/.keep
create bin
create bin/bundle
create bin/rails
create bin/rake
create bin/setup
create bin/update
create bin/yarn
create config
create config/routes.rb
create config/application.rb
create config/environment.rb
create config/secrets.yml
create config/cable.yml
create config/puma.rb
create config/spring.rb
create config/environments
create config/environments/development.rb
create config/environments/production.rb
create config/environments/test.rb
create config/initializers
create config/initializers/application_controller_renderer.rb
create config/initializers/assets.rb
create config/initializers/backtrace_silencers.rb
create config/initializers/cookies_serializer.rb
create config/initializers/cors.rb
create config/initializers/filter_parameter_logging.rb
create config/initializers/inflections.rb
create config/initializers/mime_types.rb
create config/initializers/new_framework_defaults_5_1.rb
create config/initializers/wrap_parameters.rb
create config/locales
create config/locales/en.yml
create config/boot.rb
create config/database.yml
create db
create db/seeds.rb
create lib
create lib/tasks
create lib/tasks/.keep
create lib/assets
create lib/assets/.keep
create log
create log/.keep
create public
create public/404.html
create public/422.html
create public/500.html
create public/apple-touch-icon-precomposed.png
create public/apple-touch-icon.png
create public/favicon.ico
create public/robots.txt
create test/fixtures
create test/fixtures/.keep
create test/fixtures/files
create test/fixtures/files/.keep
create test/controllers
create test/controllers/.keep
create test/mailers
create test/mailers/.keep
create test/models
create test/models/.keep
create test/helpers
create test/helpers/.keep
create test/integration
create test/integration/.keep
create test/test_helper.rb
create test/system
create test/system/.keep
create test/application_system_test_case.rb
create tmp
create tmp/.keep
create tmp/cache
create tmp/cache/assets
create vendor
create vendor/.keep
create package.json
remove config/initializers/cors.rb
remove config/initializers/new_framework_defaults_5_1.rb
bundle install --local
Resolving dependencies...
Using rake 12.0.0
Using concurrent-ruby 1.0.5
Using i18n 0.8.1
Using minitest 5.10.1
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubi 1.6.0
Using mini_portile2 2.1.0
Using rack 2.0.1
Using nio4r 2.0.0
Using websocket-extensions 0.1.2
Using mime-types-data 3.2016.0521
Using arel 8.0.0
Using public_suffix 2.0.5
Using bundler 1.14.6
Using byebug 9.0.6
Using ffi 1.9.18
Using coffee-script-source 1.12.2
Using execjs 2.7.0
Using method_source 0.8.2
Using thor 0.19.4
Using debug_inspector 0.0.2
Using multi_json 1.12.1
Using pg 0.19.0
Using puma 3.8.2
Using rubyzip 1.2.1
Using sass 3.4.23
Using websocket 1.2.4
Using sqlite3 1.3.13
Using turbolinks-source 5.0.0
Using tzinfo 1.2.2
Using nokogiri 1.7.1
Using rack-test 0.6.3
Using sprockets 4.0.0.beta4
Using websocket-driver 0.6.5
Using mime-types 3.1
Using addressable 2.5.0
Using childprocess 0.6.2
Using rb-inotify 0.9.7 from source at `/home/rubys/git/rb-inotify`
Using coffee-script 2.4.1
Using uglifier 3.1.9
Using queue_classic 3.2.0.RC1 from source at `/home/rubys/git/queue_classic`
Using turbolinks 5.0.1
Using activesupport 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using loofah 2.0.3
Using xpath 2.0.0
Using mail 2.6.4
Using selenium-webdriver 3.3.0
Using rails-dom-testing 2.0.2
Using globalid 0.3.7
Using activemodel 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using jbuilder 2.6.3
Using spring 2.0.1
Using rails-html-sanitizer 1.0.3
Using capybara 2.13.0
Using activejob 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using activerecord 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using actionview 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using actionpack 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using actioncable 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using actionmailer 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using railties 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using sprockets-rails 3.2.0
Using coffee-rails 4.2.1
Using web-console 3.4.0 from source at `/home/rubys/git/web-console`
Using rails 5.1.0.beta1 from source at `/home/rubys/git/rails`
Using sass-rails 6.0.0.beta1 from source at `/home/rubys/git/sass-rails`
Bundle complete! 16 Gemfile dependencies, 67 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
See what files were created
ls -p
app/ config.ru Gemfile.lock package.json README.md vendor/
bin/ db/ lib/ public/ test/
config/ Gemfile log/ Rakefile tmp/
Create a simple controller
rails generate controller Say hello goodbye
create app/controllers/say_controller.rb
route get 'say/goodbye'
route get 'say/hello'
invoke erb
create app/views/say
create app/views/say/hello.html.erb
create app/views/say/goodbye.html.erb
invoke test_unit
create test/controllers/say_controller_test.rb
invoke helper
create app/helpers/say_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/say.coffee
invoke scss
create app/assets/stylesheets/say.scss
edit app/controllers/say_controller.rb
class SayController < ApplicationController
def hello
end
def goodbye
end
end
Start the server.
Attempt to fetch the file - note that it is missing
get /say/hello
Say#hello
Find me in app/views/say/hello.html.erb
Replace file with a simple hello world
edit app/views/say/hello.html.erb
<h1>Hello from Rails!</h1>
This time it works!
get /say/hello
Hello from Rails!
Add a simple expression
edit app/views/say/hello.html.erb
<h1>Hello from Rails!</h1>
<p>
It is now <%= Time.now %>
</p>
get /say/hello
Hello from Rails!
It is now 2017-03-20 00:41:07 -0400
Evaluate the expression in the controller.
edit app/controllers/say_controller.rb
class SayController < ApplicationController
def hello
@time = Time.now
end
def goodbye
end
end
Reference the result in the view.
edit app/views/say/hello.html.erb
<h1>Hello from Rails!</h1>
<p>
It is now <%= @time %>
</p>
get /say/hello
Hello from Rails!
It is now 2017-03-20 00:41:07 -0400
Replace the goodbye template
edit app/views/say/goodbye.html.erb
<h1>Goodbye!</h1>
<p>
It was nice having you here.
</p>
get /say/goodbye
Goodbye!
It was nice having you here.
Add a link from the hello page to the goodbye page
edit app/views/say/hello.html.erb
<h1>Hello from Rails!</h1>
<p>
It is now <%= @time %>
</p>
<p>
Time to say
<%= link_to "Goodbye", say_goodbye_path %>!
</p>
get /say/hello
Hello from Rails!
It is now 2017-03-20 00:41:07 -0400
Time to say
Goodbye!
Add a link back to the hello page
edit app/views/say/goodbye.html.erb
<h1>Goodbye!</h1>
<p>
It was nice having you here.
</p>
<p>
Say <%= link_to "Hello", say_hello_path %> again.
</p>
get /say/goodbye
Goodbye!
It was nice having you here.
Say Hello again.
Intentionally introduce a typo in the code
edit app/controllers/say_controller.rb
class SayController < ApplicationController
def hello
@time = Time.know
end
def goodbye
end
end
get /say/hello
HTTP Response Code: 500
NoMethodError
in SayController#hello
undefined method `know' for Time:Class
Did you mean? now
Extracted source (around line #4):
2
3
4
5
6
7
|
def hello
#START_HIGHLIGHT
@time = Time.know
#END_HIGHLIGHT
end
|
Extracted source (around line #4):
2
3
4
5
6
7
|
module BasicImplicitRender # :nodoc:
def send_action(method, *args)
super.tap { default_render unless performed? }
end
def default_render(*args)
|
Extracted source (around line #186):
184
185
186
187
188
189
|
# which is *not* necessarily the same as the action name.
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override
|
Extracted source (around line #30):
28
29
30
31
32
33
|
def process_action(*) #:nodoc:
self.formats = request.formats.map(&:ref).compact
super
end
# Check for double render errors and set the content_type after rendering.
|
Extracted source (around line #20):
18
19
20
21
22
23
|
def process_action(*args)
run_callbacks(:process_action) do
super
end
end
|
Extracted source (around line #131):
129
130
131
132
133
134
|
if next_sequence.final?
next_sequence.invoke_before(env)
env.value = !env.halted && (!block_given? || yield)
next_sequence.invoke_after(env)
env.value
else
|
Extracted source (around line #19):
17
18
19
20
21
22
|
# process_action callbacks around the normal behavior.
def process_action(*args)
run_callbacks(:process_action) do
super
end
end
|
Extracted source (around line #20):
18
19
20
21
22
23
|
private
def process_action(*args)
super
rescue Exception => exception
request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
rescue_with_handler(exception) || raise
|
Extracted source (around line #32):
30
31
32
33
34
35
|
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
begin
result = super
payload[:status] = response.status
result
ensure
|
Extracted source (around line #164):
162
163
164
165
166
167
|
def instrument(name, payload = {})
if notifier.listening?(name)
instrumenter.instrument(name, payload) { yield payload if block_given? }
else
yield payload if block_given?
end
|
Extracted source (around line #21):
19
20
21
22
23
24
|
listeners_state = start name, payload
begin
yield payload
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
|
Extracted source (around line #164):
162
163
164
165
166
167
|
def instrument(name, payload = {})
if notifier.listening?(name)
instrumenter.instrument(name, payload) { yield payload if block_given? }
else
yield payload if block_given?
end
|
Extracted source (around line #30):
28
29
30
31
32
33
|
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
begin
result = super
payload[:status] = response.status
|
Extracted source (around line #248):
246
247
248
249
250
251
|
request.filtered_parameters.merge! wrapped_filtered_hash
end
super
end
private
|
Extracted source (around line #22):
20
21
22
23
24
25
|
# and it won't be cleaned up by the method below.
ActiveRecord::LogSubscriber.reset_runtime
super
end
def cleanup_view_runtime
|
Extracted source (around line #124):
122
123
124
125
126
127
|
@_response_body = nil
process_action(action_name, *args)
end
# Delegates to the class' ::controller_path
|
Extracted source (around line #30):
28
29
30
31
32
33
|
def process(*) #:nodoc:
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
super
ensure
I18n.config = old_config
end
|
Extracted source (around line #189):
187
188
189
190
191
192
|
set_request!(request)
set_response!(response)
process(name)
request.commit_flash
to_a
end
|
Extracted source (around line #253):
251
252
253
254
255
256
|
middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
else
new.dispatch(name, req, res)
end
end
end
|
Extracted source (around line #49):
47
48
49
50
51
52
|
def dispatch(controller, action, req, res)
controller.dispatch(action, req, res)
end
end
|
Extracted source (around line #31):
29
30
31
32
33
34
|
controller = controller req
res = controller.make_response! req
dispatch(controller, params[:action], req, res)
rescue ActionController::RoutingError
if @raise_on_name_error
raise
|
Extracted source (around line #46):
44
45
46
47
48
49
|
req.path_parameters = set_params.merge parameters
status, headers, body = route.app.serve(req)
if "pass" == headers["X-Cascade"]
req.script_name = script_name
|
Extracted source (around line #33):
31
32
33
34
35
36
|
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
path_info = req.path_info
script_name = req.script_name
|
Extracted source (around line #33):
31
32
33
34
35
36
|
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
path_info = req.path_info
script_name = req.script_name
|
Extracted source (around line #836):
834
835
836
837
838
839
|
req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
def recognize_path(path, environment = {})
|
Extracted source (around line #25):
23
24
25
26
27
28
|
def call(env)
status, headers, body = @app.call(env)
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
original_body = body
|
Extracted source (around line #25):
23
24
25
26
27
28
|
case env[REQUEST_METHOD]
when "GET", "HEAD"
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
if status == 200 && fresh?(env, headers)
status = 304
|
Extracted source (around line #12):
10
11
12
13
14
15
|
def call(env)
status, headers, body = @app.call(env)
if env[REQUEST_METHOD] == HEAD
[
|
Extracted source (around line #222):
220
221
222
223
224
225
|
req = make_request env
prepare_session(req)
status, headers, body = app.call(req.env)
res = Rack::Response::Raw.new status, headers
commit_session(req, res)
[status, headers, body]
|
Extracted source (around line #216):
214
215
216
217
218
219
|
def call(env)
context(env)
end
def context(env, app=@app)
|
Extracted source (around line #613):
611
612
613
614
615
616
|
request = ActionDispatch::Request.new env
status, headers, body = @app.call(env)
if request.have_cookie_jar?
cookie_jar = request.cookie_jar
|
Extracted source (around line #556):
554
555
556
557
558
559
|
@last_check = mtime
end
@app.call(env)
end
private
|
Extracted source (around line #26):
24
25
26
27
28
29
|
result = run_callbacks :call do
begin
@app.call(env)
rescue => error
end
end
|
Extracted source (around line #97):
95
96
97
98
99
100
|
if callbacks.empty?
yield if block_given?
else
env = Filters::Environment.new(self, false, nil)
next_sequence = callbacks.compile
|
Extracted source (around line #24):
22
23
24
25
26
27
|
def call(env)
error = nil
result = run_callbacks :call do
begin
@app.call(env)
rescue => error
|
Extracted source (around line #12):
10
11
12
13
14
15
|
state = @executor.run!
begin
response = @app.call(env)
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
ensure
state.complete! unless returned
|
Extracted source (around line #59):
57
58
59
60
61
62
|
def call(env)
request = ActionDispatch::Request.new env
_, headers, body = response = @app.call(env)
if headers["X-Cascade"] == "pass"
body.close if body.respond_to?(:close)
|
Extracted source (around line #135):
133
134
135
136
137
138
|
def call_app(env)
@app.call(env)
rescue => e
throw :app_exception, e
end
|
Extracted source (around line #28):
26
27
28
29
30
31
|
end
status, headers, body = call_app(env)
if session = Session.from(Thread.current) and acceptable_content_type?(headers)
response = Response.new(body, status, headers)
|
Extracted source (around line #18):
16
17
18
19
20
21
|
def call(env)
app_exception = catch :app_exception do
request = create_regular_or_whiny_request(env)
return call_app(env) unless request.from_whitelisted_ip?
|
Extracted source (around line #18):
16
17
18
19
20
21
|
def call(env)
app_exception = catch :app_exception do
request = create_regular_or_whiny_request(env)
return call_app(env) unless request.from_whitelisted_ip?
|
Extracted source (around line #31):
29
30
31
32
33
34
|
def call(env)
request = ActionDispatch::Request.new env
@app.call(env)
rescue Exception => exception
if request.show_exceptions?
render_exception(request, exception)
|
Extracted source (around line #36):
34
35
36
37
38
39
|
instrumenter.start "request.action_dispatch", request: request
logger.info { started_request_message(request) }
resp = @app.call(env)
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
resp
rescue Exception
|
Extracted source (around line #24):
22
23
24
25
26
27
|
if logger.respond_to?(:tagged)
logger.tagged(compute_tags(request)) { call_app(request, env) }
else
call_app(request, env)
end
|
Extracted source (around line #69):
67
68
69
70
71
72
|
def tagged(*tags)
formatter.tagged(*tags) { yield self }
end
def flush
|
Extracted source (around line #26):
24
25
26
27
28
29
|
def tagged(*tags)
new_tags = push_tags(*tags)
yield self
ensure
pop_tags(new_tags.size)
end
|
Extracted source (around line #69):
67
68
69
70
71
72
|
def tagged(*tags)
formatter.tagged(*tags) { yield self }
end
def flush
|
Extracted source (around line #24):
22
23
24
25
26
27
|
if logger.respond_to?(:tagged)
logger.tagged(compute_tags(request)) { call_app(request, env) }
else
call_app(request, env)
end
|
Extracted source (around line #13):
11
12
13
14
15
16
|
::Rails.logger.silence { @app.call(env) }
else
@app.call(env)
end
end
end
|
Extracted source (around line #79):
77
78
79
80
81
82
|
req = ActionDispatch::Request.new env
req.remote_ip = GetIp.new(req, check_ip, proxies)
@app.call(req.env)
end
# The GetIp class exists as a way to defer processing of the request data
|
Extracted source (around line #25):
23
24
25
26
27
28
|
req = ActionDispatch::Request.new env
req.request_id = make_request_id(req.x_request_id)
@app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
end
private
|
Extracted source (around line #22):
20
21
22
23
24
25
|
end
@app.call(env)
end
def method_override(env)
|
Extracted source (around line #22):
20
21
22
23
24
25
|
def call(env)
start_time = Utils.clock_time
status, headers, body = @app.call(env)
request_time = Utils.clock_time - start_time
unless headers.has_key?(@header_name)
|
Extracted source (around line #27):
25
26
27
28
29
30
|
def call(env)
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
|
Extracted source (around line #12):
10
11
12
13
14
15
|
state = @executor.run!
begin
response = @app.call(env)
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
ensure
state.complete! unless returned
|
Extracted source (around line #125):
123
124
125
126
127
128
|
end
@app.call(req.env)
end
end
end
|
Extracted source (around line #111):
109
110
111
112
113
114
|
def call(env)
status, headers, body = @app.call(env)
if body.respond_to?(:to_path)
case type = variation(env)
when 'X-Accel-Redirect'
|
Extracted source (around line #522):
520
521
522
523
524
525
|
def call(env)
req = build_request env
app.call req.env
end
# Defines additional Rack env configuration that is added on each call.
|
Extracted source (around line #224):
222
223
224
225
226
227
|
def call(env)
env[Const::PUMA_CONFIG] = @config
@app.call(env)
end
end
|
Extracted source (around line #600):
598
599
600
601
602
603
|
begin
begin
status, headers, res_body = @app.call(env)
return :async if req.hijacked
|
Extracted source (around line #435):
433
434
435
436
437
438
|
while true
case handle_request(client, buffer)
when false
return
when :async
|
Extracted source (around line #299):
297
298
299
300
301
302
|
else
if process_now
process_client client, buffer
else
client.set_timeout @first_data_timeout
@reactor.add client
|
Extracted source (around line #120):
118
119
120
121
122
123
|
begin
block.call(work, *extra)
rescue Exception => e
STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
end
|
Rails.root: /home/rubys/git/awdwr/edition4/work-240/demo1
Request
Parameters:
None
_csrf_token: "W3tAhfjA/ZZOjoA/C+g8C49Y9qfd9/6BpVc4d2meTUo="
session_id: "2c787dc010144ec8ba1cd5f149b8c327"
GATEWAY_INTERFACE: "CGI/1.2"
HTTP_ACCEPT: "text/html"
HTTP_ACCEPT_ENCODING: "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
HTTP_VERSION: "HTTP/1.1"
ORIGINAL_SCRIPT_NAME: ""
REMOTE_ADDR: "127.0.0.1"
SERVER_NAME: "localhost"
SERVER_PROTOCOL: "HTTP/1.1"
Response
Headers:
None
Intentionally introduce a typo in a URL
get /say/hullo
HTTP Response Code: 404
No route matches [GET] "/say/hullo"
Rails.root: /home/rubys/git/awdwr/edition4/work-240/demo1
Routes
Routes match in priority from top to bottom
Request
Parameters:
None
_csrf_token: "W3tAhfjA/ZZOjoA/C+g8C49Y9qfd9/6BpVc4d2meTUo="
session_id: "2c787dc010144ec8ba1cd5f149b8c327"
GATEWAY_INTERFACE: "CGI/1.2"
HTTP_ACCEPT: "text/html"
HTTP_ACCEPT_ENCODING: "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
HTTP_VERSION: "HTTP/1.1"
ORIGINAL_SCRIPT_NAME: ""
REMOTE_ADDR: "127.0.0.1"
SERVER_NAME: "localhost"
SERVER_PROTOCOL: "HTTP/1.1"
Response
Headers:
None
6.1 Iteration A1: Creating the Products Maintenance Application
Table of Contents