Skip to content

Latest commit

 

History

History
1517 lines (1090 loc) · 70.9 KB

File metadata and controls

1517 lines (1090 loc) · 70.9 KB

You will continue to develop your application from the point you arrived at the end of week 1. The material that follows comes with the assumption that you have done all the exercises of the previous week. In case you have not done all of them, you can take the sample answer of the previous week. If you already got most of the previous week exercises done, it might be easier if you complement your own answer with the help of the material.

Sensible editor

Hopefully you are already using a sensible editor at this point, that is, something else than nano, gedit or notepad. Recommendable editors include RubyMine and Visual Studio Code. See here for more.

Nowadays, Visual Studio Code is very popular. If you use VSC, it is very much recommended to install the Ruby plugin.

In the end, when choosing an editor, the most important aspect is that is pleasant to use.

Application layout

You want to put a navigation bar in your page like modern websites, placing a link to the lists with beers and breweries at the top of all pages.

You can generate a navigation bar with the help of the method link_to and path helpers by adding the following links to each page:

<%= link_to 'breweries', breweries_path %>
<%= link_to 'beers', beers_path %>

If you had sharp eyes you might have noticed last week already that view templates do not contain all the HTML code of the pages. For instance, the view template for one beer, /app/views/beers/show.html.erb, looks like below:

<p style="color: green"><%= notice %></p>

<%= render @beer %>

<div>
  <%= link_to "Edit this beer", edit_beer_path(@beer) %> |
  <%= link_to "Back to beers", beers_path %>

  <%= button_to "Destroy this beer", @beer, method: :delete %>
</div>

If you look at the HTML code of the page of one beer using the view source code option, you will see, that the page has much more HTML code than it is defined in the template (a part of the head contents has been removed):

<!DOCTYPE html>
<html>
<head>
  <title>Ratebeer</title>
  <link data-turbolinks-track="true" href="/assets/application.css?body=1" media="all" rel="stylesheet" />
  <script data-turbolinks-track="true" src="/assets/jquery.js?body=1"></script>
  <meta content="authenticity_token" name="csrf-param" />
  <meta content="hZaC8o95xUbekA3PTsVZ+JmkVj9CCn5a4Kw8tF96WOU=" name="csrf-token" />
</head>
<body>

<p id="notice"></p>

<p>
  <strong>Name:</strong>
  Iso 3
</p>

<p>
  <strong>Style:</strong>
  Lager
</p>

<p>
  <strong>Brewery:</strong>
  1
</p>

<a href="/beers/1/edit">Edit</a> |
<a href="/beers">Back</a>


</body>
</html>

The page contains the type definition of the document, the head element which defines the style files and Javascript files to use, and the body element which defines the page contents (see more at http://www.w3.org/community/webed/wiki/HTML/Training).

The view template of the beer page contains only the HTML code which comes with the body element.

It's typical that all the pages of the application are the same except for the contents of the body element. In Rails, we can define the parts in common to all pages in the application layout, the file app/views/layouts/application.html.erb. The file contents will be the following by default:

<!DOCTYPE html>
<html>
  <head>
    <title>Ratebeer</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

The auxiliary methods inside the Head element define the style and javascript files which are used by the application. The auxiliary method csrf_meta_tags adds to the file the logic to eliminate CSRF attacks (see the link for more information). As you may have guessed, the yield command inside the body element helps to render the contents defined by the view template of each page.

We can display a navigation bar in all pages by modifying the body element of our application layout in the following way:

<body>
  <div class="navibar">
    <%= link_to 'breweries', breweries_path %>
    <%= link_to 'beers', beers_path %>
  </div>

  <%= yield %>

</body>

The navigation bar has been set up in the div element which contains the navibar class. We can modify its layout with the help of our CSS files.

Add the following to the file app/assets/stylesheets/application.css:

.navibar {
  padding: 10px;
  background: #EFEFEF;
}

When you reload the page, you'll notice your application will almost look professional.

routes.rb

The Routing component on Rails (see http://api.rubyonrails.org/classes/ActionDispatch/Routing.html, http://guides.rubyonrails.org/routing.html) is in charge of handling the HTTP requests of the application and of routing them to the appropriate method controller.

The file config/routes.rb contains the information of how to route the requests to the various URLs. At this point, the file contents look like this:

Rails.application.routes.draw do
  resources :beers
  resources :breweries
end

Later on, we will get to know the routes which are added by the method resources.

Let us get started by setting the webpage containing a list of the breweries as the default page of the application. This will happen by adding the following line to the routes file

root 'breweries#index'

The address http://localhost:3000/ will now lead to a page with all breweries.

What we wrote above is but the classier way to say:

get '/', to: 'breweries#index'

this means that when an HTTP GET request arrives to the path '/', it is routed and handled by the index method of the BreweriesController.

If we read the documentation, we should pay attention that a controller's methods are often called actions, in Rails. In any case, we have decided to use the name controller method in the course.

Similarly, you could also add the following line to routes.rb

get 'all_beers', to: 'beers#index'

In such case, the GET requests to the URL http://localhost:3000/all_beers would lead to the page of all beers. Try that it works.

An interesting thing of the routes.rb file is that, even though it looks like a configuration file of pure text, all the contents are written in Ruby. The file lines are method calls. For instance the line

get 'all_beers', to: 'beers#index'

calls the get method with parameters that are the string '/all_beers' and the hash to: 'beers#index'. This uses a newer syntax for hash expressions. If we use the old syntax, the part of the hash which defines the routing would be written :to => 'beers#index', and the line of routes.rb would be:

get 'all_beers', :to => 'beers#index'

we could also use brackets in the method call, and define the hash using curly brackets. The following might look clumsy, but it is correct to define the route:

get( 'all_beers', { :to => 'beers#index' } )

The elastic syntax (together with other characteristic features of the language) allows for a form which aims at the fluency of natural languages while configuring and programming applications. The style is well known in English with the name Internal DSL, see http://martinfowler.com/bliki/InternalDslStyle.html.

Rating beers

Let us add the possibility to rate beers, that is to say, to give them a grade from 0 to 50. We will not use the generator that we have discovered last week (rails generate scaffold...), but we are going to create one ourselves, instead.

We want that all the rating are available at the address http://localhost:3000/ratings. Let us try to see what happens in our browser when we try to reach the URL.

The result in the error expression No route matches [GET] "/ratings" which tells us that the HTTP GET request which was done to the address was not matched by any defined route.

Let us add the route by creating the following line in the routes file:

get 'ratings', to: 'ratings#index'

In Rails conventions, this defines that the index method of the RatingsController class will be in charge of the ratings page.

Attention: if you wrote match 'ratings' => 'ratings#index', you would get almost the same result. As it is typical in Rails, there are different ways to define the same thing in routes.rb, too.

Check out the page again in you browser.

The error exception has changed to a new form, uninitialized constant RatingsController. This means that when the GET request arrives to the ratings address, the defined route tries to lead it so that it would be handled by the index method of the controller which is defined in the class RatingsController.

Let us define a controller in the file /app/controllers/ratings_controller.rb.

class RatingsController < ApplicationController
  def index
  end
end

Pay attention to the name conventions and file location – Rails looks for the controller in the folder /app/controllers. If you place the controller somewhere else, Rails will not find it.

Try out the page with the browser once more.

You will receive the following error exception

RatingsController#index is missing a template for request formats: text/html

This happens because Rails tries to render the default view template which corresponds to the controller method and that should be located in /app/views/ratings/index.html.erb. Such file is not found, however.

Let us create the file /app/views/ratings/index.html.erb with the following contents (you will also need to create the directory /app/views/ratings):

<h2>List of ratings</h2>

<p>To be completed...</p>

and now the page works!

Note again Rails conventions and the file location, which is defined carefully. Because it is a view template which is called by the RatingsController, the view template is placed in the directory /views/ratings.

Another reminder from last week: the index control method renders the index view (which is located in the appropriate directory) at the end of the execution by default. The code

class RatingsController < ApplicationController
  def index
  end
end

does the same thing as:

class RatingsController < ApplicationController
  def index
    render :index    # renders the view template /app/views/ratings/index.html
  end
end

In any case, we do not explicitly call the render method if the default file is rendered – that is to say the template with the same name of the controller method.

Creating the model by hand: done, almost...

One beer has many ratings, which means that the object model should be updated to look as follows:

One beer has many ratings

We need a database table and the corresponding model object.

It is good to use migrations always when you want to make changes on Rails, for instance when adding a table. The migrations are files which have to be placed in the directory db/migrate, and where we note the Ruby operations which modify the database. We will better familiarize ourselves with migrations later on. Now, we use Rails' ready-made model generator to create our model. The generator not only creates a model object, but it also generates automatically the migration we need.

Ratings have an integer score and a foreign key, which links to the rated beer. According to Rails conventions, the foreign key name has to be beer_id.

You can create the model and the migration which generates the database by giving the following command in the command line:

rails g model Rating score:integer beer_id:integer

and the database table by executing the following migration in the command line

rails db:migrate

Differently than the scaffold generator which we used last week, the model generator does not create a controller or view templates.

A reminder from last week: the files generated by Rails generators (scaffold, model, ...) can be deleted with the command destroy:

rails destroy model Rating

If you have already executed the migration, and you notice that the code created by the generator has to be destroyed, it is extremely important that you first cancel the migration with the command

rails db:rollback

In order to establish the connections at object level too (check last week's material, the classes have to be updated in the following way

class Beer < ApplicationRecord
  belongs_to :brewery
  has_many :ratings
end

class Rating < ApplicationRecord
  belongs_to :beer
end

Each beer has many ratings and a rating belongs to one sole beer always.

Start the Rails console running the command rails c from the command line. Notice that if your console was open already, you can start to use the new code in the console by running reload!. Create a few ratings:

> b = Beer.first
> b.ratings.create score: 10
> b.ratings.create score: 21
> b.ratings.create score: 17

The ratings are added to the first beer which is found in the database. Notice the way it was created; you could have run the same thing with the more complex

b.ratings << Rating.create(score:15)

Missing foreign key

Let's try to create a beer without a brewery:

irb(main)> b = Beer.create name:"anonymous", style: "watery"
=> #<Beer:0x00007f4444abc8b0 id: nil, name: "anonymous", style: "watery", brewery_id: nil, created_at: nil, updated_at: nil>
irb(main)>

id and the time stamps do not get any values. It looks like the beer wasn't saved into the databse at all.

If we use beer method errors, we find the reason for the failed save.

irb(main)> b.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=brewery, type=blank, options={:message=>:required}>]>

So the beer won't be saved to the database without information about its brewery. We can fix this by defining the brewery and calling the method save for the beer:

> b.brewery = Brewery.find_by(name: 'Koff')
> b.save
   (0.1ms)  begin transaction
  Beer Create (1.9ms)  INSERT INTO "beers" ("name", "style", "brewery_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "anonymous"], ["style", "watery"], ["brewery_id", 1], ["created_at", "2022-09-11 18:21:40.830949"], ["updated_at", "2022-09-11 18:21:40.830949"]]
   (0.8ms)  commit transaction

The reason for failing to save the beer was that by default Rails demands that if an object refers to another object via a foreign key and belongs_to is used in the code to form the association (like we do in the case of beers)

class Beer < ApplicationRecord
  belongs_to :brewery

  # ...
end

the foreign key cannot be uninitialized.

Exercise 1

The console use routine is extremely important for Rails developers. Do the following things by hand:

create a new brewery "BrewDog", the founding date is 2007
add two beers to the brewery

  • Punk IPA (style IPA)
  • Nanny State (style low alcohol) add a couple of ratings to both beers

Go through last week material in case you need and check the parts about console use.

Return this exercise by adding the directory exercises to your application. The directory has to contain the file exercise1, with the copy-pasted console session

Our database contains ratings now, and we want to make sure they appear on a page with all ratings.

Exercise 2

Add all the ratings on a ratings page. You can use the brewery controller index method and the corresponding template as model. When you make the rating list, use the following style, for instance

<ul>
 <% @ratings.each do |rating| %>
   <li> <%= rating %> </li>
 <% end %>
</ul>

Also add the information about the total amount of ratings to the page.

At this point, the page should look more or less like this:

picture

The rating is rendered in a quite unpleasant way. This depends on the fact that the li element contains only the object name, and because we haven't defined the method to_s which turns the ratings to strings, the method in use is the default to_s method which was inherited by all classes from the parent class Object.

Soon, we will define a partial file for the ratings which breaks down the code and lets us create an easily readable display for our ratings. Before doing it, we will first see a couple of things about object method definitions.

A couple of clarifications about Rails model objects

Spend a second to inspect the class Brewery:

class Brewery < ApplicationRecord
  has_many :beers
end

The brewery has a name and a founding year. We can reach them by hand from the console:

> b = Brewery.first
> b.name
=> "Koff"
> b.year
=> 1897
>

Technically, b.year is a method call. For each column which is defined by the schema of the database table, Rails creates a field into the model object. It creates an attribute and the method to read and change the attribute value. These automatically generated methods look more or less as follows:

class Brewery < ApplicationRecord
  # ..

  def year
    read_attribute(:year)
  end

  def year=(value)
    write_attribute(:year, value)
  end
end

The methods helps us to read and change the value of the object attribute. The method which changes the value does not yet implement the change in the database, which happens only when we call the save method, they are an example of 'getters and setters' which are generated automatically.

Outside the object, we retrieve object attributes by using 'dot notation':

b.year

What about inside the object? Create a method to demonstrate how attributes are handled inside brewery:

class Brewery < ApplicationRecord
  has_many :beers

  def print_report
    puts name
    puts "established at year #{year}"
    puts "number of beers #{beers.count}"
  end
end

As it is also in Java for instance, methods can be called with their name from within the object (beers is a method, too!)

You find an example on how to use the method:

> b = Brewery.first
> b.print_report
Koff
established at year 1897
number of beers 2

You could have called the methods from inside the object by using Ruby's 'this,' that is, the object self reference:

  def print_report
    puts self.name
    puts "established at year #{self.year}"
    puts "number of beers #{self.beers.count}"
  end

Next, create a method to 'restart' the brewery, in such case the establishment year changes to year 2022:

def restart
  year = 2022
  puts "changed year to #{year}"
end

Try it out:

> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2022
> b.year
=> 1897
>

as you notice, the year change does not work as we expected! As far as the method restart is concerned, there is no method call with year = 2022

def year=(value)

There is no method call which could assign a new value to the attribute. Instead, a local variable called year is created in the method, and it is given value 2022.

If you want to make the assignment work, you have to call the method through a self reference:

def restart
  self.year = 2022
  puts "changed year to #{year}"
end

The functionality will now fulfill your expectations:

> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2022
> b.year
=> 2022
>

Attention: In Ruby, instance variable are defined with an @ character at the beginning. Instance variables are not the same thing as the object attributes which are saved in the database thanks to ActiveRecord. The following code would not work as you could expect:

  def restart
    @year = 2022
    puts "changed year to #{@year}"
  end

The code inside the brewery is an attribute which is saved in the database by ActiveRecord, whereas @year is an instance variable of the object. Instance variables are not much used in Rails. Instance variables are used on Rails mostly to transmit information from the controllers to the views.

Exercise 3

Change the ratings page so that each rating is displayed better as a string, e.g. "karhu 35", which contains the name of the rated beer, followed by its rating value.

The following link might be useful to form strings: https://github.com/mluukkai/WebPalvelinohjelmointi2023/blob/main/web/rubyn_perusteita.md#merkkijonot

You can do everything directly in file views/partials/index.html.erb or optionally you create a partial template for rating. This handles displaying a single rating.

You can use eg. _beer.html.erb and the responding index.html.erb files as a guide. Remember how partials files are named!

After you have done the exercise, the rating pages should look more or less like this:

picture

Attention: when you create new code in your application, it is a good practice to make trials by hand in your console. Below, we try to use the default method to_s to return the value of the rating:

> r = Rating.last
> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
>

Now we will define a to_s method for the rating:

class Rating < ApplicationRecord
  belongs_to :beer

  def to_s
    "written rating"
  end
end

and we try to run it again from the console:

> r.to_s
=> "#<Rating:0x007f8054b1cb10>"

It looks like the change has not been implemented anyway, what is wrong?

In order to make sure the change of the code is implemented, you have to reload the new code in the console using the command reload!, then you should retrieve the object from the database again and use this one.

> reload!
Reloading...
=> true
> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
> r = Rating.last
> r.to_s
=> "written rating"
>

As you see above, reloading the code is not enough in itself, because the object stored in the variable r contains the old code still.

Exercise 4

Create the method average_rating to the class Beer to find the average value of the beer ratings. Add the average value to the beer page if the beer has ratings

The contents of the view template can be made conditional with something like this

<% if beer.ratings.empty? %>
 beer has not yet been rated!
<% else %>
 beer has some ratings
<% end %>

Remember to return the rating value as a floating point number. For this you can use the to_f method.

The beer page should look more or less like the picture below, after you have done the exercise (notice that after last week, you could be showing the brewery ID instead of the brewery name on the page. If so, change your view so that it corresponds to the picture):

picture

Exercise 5

The module enumerable (see https://ruby-doc.org/core-3.1.2/Enumerable.html) contains a large extent of auxiliary methods to parse object collections.

Object collection classes can include the module enumerable functionality by inheriting it.

Get acquainted with the methods map and reduce (see for instance reduce, map and google for further information) and change (in case you need) the method which calculates the rating avarage value so that it makes use of reduce or map and sum.

Calculating the average value is easier in this case if you use ActiveRecord methods, see http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html

Using the console, add a rating to one previously unrated beer. The beer page should now look like the one below:

picture

The page has one small, but annoying grammar mistake:

beer has 1 ratings

Exercise 6

Get acquainted with the auxiliary method pluralize which is ready made in Rails (http://apidock.com/rails/ActionView/Helpers/TextHelper/pluralize) and use it to make sure the page beer page is grammatically correct (in case there is one rating, the method should print 'beer has 1 rating.')

Form and post

Make it possible that ratings are created by hand in your application from the www-page.

According to Rails conventions, the form to create a Rating object has to be found at the address ratings/new, and the form can be accessed thanks to the new method of the ratings controller.

Create the appropriate route in routes.rb

get 'ratings/new', to:'ratings#new'

We add the new method to the ratings controller (whose actual name is RatingsController). The method makes sure the form is rendered. It looks simple:

  def new
    @rating = Rating.new
  end

The method only creates a new Rating object and passes it as @rating to the new.html.erb view template which is rendered by default through the method. The object is created with a new command, so it is not saved into the database.

Create now the next view, the file /app/views/ratings/new.html.erb:

<h2>Create new rating</h2>

<%= form_for(@rating) do |f| %>
  beer id: <%= f.number_field :beer_id %>
  score: <%= f.number_field :score %>
  <%= f.submit %>
<% end %>

Go now to the page which contains the form, at the address http://localhost:3000/ratings/new

The HTML code which is rendered with the help of the view looks more or less like the one below (you find the code by going to the page and choosing view page source from the browser):

<form action="/ratings" method="post">
  beer id: <input name="rating[beer_id]" type="number" />
  score: <input name="rating[score]" type="number" />
  <input name="commit" type="submit" value="Create Rating" />
</form>

As you can see, a normal HTML form is generated (you find more details at http://www.w3.org/community/webed/wiki/HTML/Training#Forms).

The address where the form is sent to is /ratings and the HTTP method used is not GET but POST. There are two fields which are numbers, and their values are sent to users with a POST call as values of the variables rating[beer_id] and rating[score].

Rails method form_for creates a form which works correctly and sends the data to the right address automatically. The form has input fields for all the attributes on the object in parameter.

You can read more about how to create forms with the form_for method at the address http://guides.rubyonrails.org/form_helpers.html#dealing-with-model-objects

If we try to create ratings,nothing seems to happen. The browser's developer console however shows us that the browser has done a POST request to http://localhost:3000/ratings but the server has replied with 404.

picture

This means we have to create a route in the file config/routes.rb for sending the form:

post 'ratings', to: 'ratings#create'

According to Rails conventions, the method which is in charge of creating a new object is called create. Make its foundation:

  def create
    raise
  end

At this point, the method does not make anything else than throwing an exception (the method call raise).

Try to send information with the form, now. The exception thrown in the controller method causes an error message. Rails adds ample diagnostics in the error page, for instance the hash which is contained by the HTTP request parameters. The diagnostics looks like the one below:

{"authenticity_token"=>"[FILTERED]",
 "rating"=>{"beer_id"=>"1", "score"=>"2"},
 "commit"=>"Create Rating"}

The information sent with the form is contained within the hash.

The hash containing the parameters is saved in the controller variable params.

The new pieces of information about the ratings are the values of the hash key :rating, and we can retrieve them with the command params[:rating]. This is an hash itself and its value is {"beer_id"=>"1", "score"=>"2"}. In other words, you can retrieve the rating with the command params[:rating][:score].

debugger

Inspect the thing with the controller by hand, making use of Rails debugger.

Rails has already configured a debugger for your use. However, Rails' default debugger doesn't perform well in some situations so let's install an alternative, pry-byebug, by adding the following to Gemfile

group :development, :test do
  gem 'pry-byebug'
end

and execute the command bundle install from the command line and restart your Rails application.

Add the command binding.pry to the beginning of the controller, at the point where you want to inspect the code.

def create
  binding.pry
end

When you create a new rating using the form, the application stops at the binding.pry command. An interactive console view will open in the terminal where Rails is running:

Started POST "/ratings" for ::1 at 2022-07-20 14:02:51 +0300
Processing by RatingsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "rating"=>{"beer_id"=>"12", "score"=>"12"}, "commit"=>"Create Rating"}
[7, 15] in ~/ratebeer/app/controllers/ratings_controller.rb
     7|     def new
     8|       @rating = Rating.new
     9|     end
    10|
    11|     def create
=>  12|       binding.pry
    13|     end
    14|
    15| end
=>#0    RatingsController#create at ~/ratebeer/app/controllers/ratings_controller.rb:12
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 73 frames (use `bt' command for all frames)
(ruby)

The arrow tells you where the execution was paused. Analyse the contents of the params variable, now:

(rdbg) params
#<ActionController::Parameters {"authenticity_token"=>"2pGKvP6I-RYAoEbZr6eJltrNZt_T0YlQvO4K7EOyMFrF1W_OzJoPTKd39LBQoMyG5u_ScQrLjztIcB8TyWpDTw", "rating"=>#<ActionController::Parameters {"beer_id"=>"12", "score"=>"12"} permitted: false>, "commit"=>"Create Rating", "controller"=>"ratings", "action"=>"create"} permitted: false>
(ruby) params[:rating][:beer_id]
"12"
(ruby) params[:rating][:score]
"12"

You can execute whatever code from the debugger console as if it was a Rails console, in case you need.

The most important commands of the debugger might be step, next, continue, and help. Step executes the next step in the code, along with the possible method calls. Next executes the whole next line. Continue lets the program execution continue in the normal way.

More info about the debugger https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem

Saving a rating

Inside the controller, params[:rating] contains all the information which is needed to create a new rating. Also, because it is a hash like {"beer_id"=>"1", "score"=>"30"}, it can be given straight as parameter to the method create. It should be possible to create a rating by using the command:

Rating.create params[:rating]  # which means the same as Rating.create beer_id:"1", score:"30"

So change your controller code to look like the following:

  def create
    Rating.create params[:rating]
  end

Try to create a new rating now. Against all our best hopes, the action fails, and we are thrown an error message

ActiveModel::ForbiddenAttributesError

What is it about?

If the command to create the rating had been

Rating.create beer_id: params[:rating][:beer_id], score: params[:rating][:score]

which means exactly the same as the form above because params[:rating] is exactly the same hash as beer_id:params[:rating][:beer_id], score:params[:rating][:score]), we wouldn't have met any error message. Because of information security issues Rails does not allow "high-handed" mass assignments (that is to say, giving all parameters as hash) of a params variable when the object is created.

Starting from Rails 4, we have to specify what contents of the hash params we can mass-assign when we create the objects. For this, the controller uses the methods require and permit of params.

The idea is that first we use require to retrieve the hash which contains the information of the object which has to be created from params:

params.require(:rating)

after this, we use permit to specify the fields where value mass assignment can be allowed:

params.require(:rating).permit(:score, :beer_id)

Our controller looks like the following:

  def create
    Rating.create params.require(:rating).permit(:score, :beer_id)
  end

More information on how to handle form parameters in https://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters.

Try now to create the rating. ATTENTION: when you use a form to create a rating, make sure that the beer ID input to the form corresponds to a beer ID which is available in the database!

Creating the rating works now, check it with the console or from the page of all ratings. At least on Chrome creating a rating causes a situation where the browser seems to stay on the same page but the page "freezes". The reason for this is revealed in the log message printed into application console:

↳ app/controllers/ratings_controller.rb:12:in `create'
No template found for RatingsController#create, rendering head :no_content
Completed 204 No Content in 55ms (ActiveRecord: 16.4ms | Allocations: 12093)

So, because no view template has been configured for the create operation, the browser sends an empty response, that is, an answer that contains no HTML code. Chrome however seems to keep the previous page displayed when it receives an empty response.

Redirecting

We could create the template now, but we decide that the user browser is redirected to the page containing all ratings after a new rating is created. Change the controller code to look like the one below:

def create
  Rating.create params.require(:rating).permit(:score, :beer_id)
  redirect_to ratings_path
end

ratings_path is a path auxiliary method which is provided by Rails, and it means the same as "/ratings"

If you have created ratings where beer_id does not reflect an existing beer ID, you will now most likely be thrown an error message. You can destroy these ratings by hand from the Rails console in the following way:

    Rating.last        # shows the rating which has been created last, check if its beer_id is incorrect
    Rating.last.delete # removes the rating which was created last

You can destroy the beerless ratings also with the following one-liner:

Rating.all.select{ |r| r.beer.nil? }.each{ |r| r.delete }

Select creates a table which will contain the collection items we have gone through if the option in the code chunk is true. r.beer.nil? returns true if the object r.beer is nil.

The command above can also be written in a shorter form like

Rating.all.select{ |r| r.beer.nil? }.each(&:delete)

What does the command redirect_to ratings_path exactly do when we use it in the controller? Usually the controller renders the appropriate view template, and the code retrieved is then returned to the browser, which renders the page on the screen.

When the browser is redirected, the server sends a response which is equipped with a status code 302, which does not contain any HTML at all. The response contains only an address to where browser will automatically make the HTTP GET request. The redirection remains unnoticed for the browser user.

Try what happens if you put something like redirect_to "http://www.cs.helsinki.fi" as final redirection when a new rating is created!

redirect_to vs render

Technically, it would have been possible to avoid using redirect and render the page with all ratings straight from the controller that creates the new rating:

def create
  Rating.create params.require(:rating).permit(:score, :beer_id)
  @ratings = Rating.all
  render :index
end

Even though the result will look exactly the same for the website visitors, the solution is not the best for a couple of reasons. First of all, you have to copy to the create method all the code contained in the index method, which is needed to form the view. (Now there is not much to copy, but the case is not always as simple)

Another reason concerns the browser's behaviour. If our controller rendered the page, and the browser user refreshed the page after creating one beer, some of the older browsers could resend the form contents. This is because the previous action of the browser, that the refreshing then re-executes, is actually the HTTP POST request that took care of sending the form.

This problem does not exist with redirections: the page shown to visitors after the POST command is the page which is retrieved with the HTTP GET triggered by the redirection.

The rule of thumb is that you should always use redirection with controllers that handle HTTP POST methods for forms. This is true not only on Rails, but more generally in Web-programming, see http://en.wikipedia.org/wiki/Post/Redirect/Get, with the only exception if the controller operation does not work for reasons like the information sent with the form is incorrect.

Let us underline this important difference once again:

  • when the controller method ends with the command render :something (which often happens implicitly), your Rails application generates an HTML page, and the server sends it to the browser to be rendered
  • when the controller finds the command redirect_to address, the server sends a redirect request together with a status code 302 to the browser, requesting the browser to make the HTTP GET request to the address defined by the controller method. This happens automatically and the browser user will not notice the redirection

Every Web developer has to understand the part above!

Path helper methods

Rails creates path methods (or path helpers) automatically to all the routes which are defined in routes.rb. Thanks to them, you will not need to hard-code the addresses of the various different pages.

For instance, the redirection address after creating a new rating could have been hard-coded like the example below, instead of using the auxiliary function ratings_path:

def create
  Rating.create params.require(:rating).permit(:score, :beer_id)
  redirect_to 'ratings'
end

As usual, hard-coding does not make sense with addresses either.

The available paths which are generated automatically can be viewed from the command line, using the command rails routes

mluukkai@melkki.~/ratebeer$ rails routes
      Prefix Verb   URI Pattern                   Controller#Action
       beers GET    /beers(.:format)              beers#index
             POST   /beers(.:format)              beers#create
    new_beer GET    /beers/new(.:format)          beers#new
   edit_beer GET    /beers/:id/edit(.:format)     beers#edit
        beer GET    /beers/:id(.:format)          beers#show
             PATCH  /beers/:id(.:format)          beers#update
             PUT    /beers/:id(.:format)          beers#update
             DELETE /beers/:id(.:format)          beers#destroy
   breweries GET    /breweries(.:format)          breweries#index
             POST   /breweries(.:format)          breweries#create
 new_brewery GET    /breweries/new(.:format)      breweries#new
edit_brewery GET    /breweries/:id/edit(.:format) breweries#edit
     brewery GET    /breweries/:id(.:format)      breweries#show
             PATCH  /breweries/:id(.:format)      breweries#update
             PUT    /breweries/:id(.:format)      breweries#update
             DELETE /breweries/:id(.:format)      breweries#destroy
        root GET    /                             breweries#index
     ratings GET    /ratings(.:format)            ratings#index
 ratings_new GET    /ratings/new(.:format)        ratings#new
             POST   /ratings(.:format)            ratings#create

For instance, the last 3 routes tell us the following things:

  • the method call ratings_path generates a link to the address "ratings" which is directed to the index method of the ratings controller.
  • the method call ratings_new_path generates a link to the address "ratings/new" which is directed to the new method of the ratings controller. This will render the rating form.
    • Attention: as you see if you compare the routes above, ratings_new_path is not the same as the creation path for new beers, for instance, we will correct the problem later
  • The POST call to the address "ratings" is directed to the create method of the ratings controller

As we have already noticed,the information of the command rails routes arrives to the web page to render in error situations. The page even provides you with an interactive tool, which you can use to see how the application routes the input example path:

picture

Exercise 7

In the page with all ratings, add a link to create a new rating. Add a link to the list with all ratings in the navigation bar of your application.

Beer selection from a list

Creating a new rating is not the nicest thing to do now, because the user has to know the beer ID. Let's change the rating so that the user can choose the beer he wants to evaluate out of a list.

If we want the form to create a list, the controller in charge of displaying the form has to retrieve the list from the database and save it into a variable. Extend the controller in the following way:

class RatingsController < ApplicationController
  def new
    @rating = Rating.new
    @beers = Beer.all
  end

  # ...
end

If you consult page http://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease and make a couple of trials, you will find out that the form to create a rating has to be modified in the following way:

<%= form_for(@rating) do |f| %>
  <%= f.select :beer_id, options_from_collection_for_select(@beers, :id, :name) %>
  score: <%= f.number_field :score %>

  <%= f.submit %>
<% end %>

This means that the value of beer_id of the form is generated with the select element of the HTML form. You can select the options of this element using the view method options_from_collection_for_select from the list of beers contained by the @beers variable. This is done by setting the beer ID (the second parameter :id) as value and the beer name (third parameter :name) is shown to the form users.

The third parameter defines what individual options are shown on the form. In this case, the result of the method name for each beer. In Ruby, references to method names are defined as symbols, that is, as strings starting with a colon.

Attention: you can test the view methods from the console too. The methods can be called through the helper object:

> b = Beer.all
> helper.options_from_collection_for_select(b, :id, :name)
=> "<option value=\"1\">Iso 3</option>\n<option value=\"2\">Karhu</option>\n<option value=\"3\">Tuplahumala</option>\n<option value=\"4\">Huvila Pale Ale</option>\n<option value=\"5\">X Porter</option>\n<option value=\"6\">Hefeweizen</option>\n<option value=\"7\">Helles</option>\n<option value=\"8\">Lite</option>\n<option value=\"9\">IVB</option>\n<option value=\"10\">Extra Light Triple Brewed</option>\n<option value=\"13\">Punk IPA</option>\n<option value=\"14\">Nanny State</option>"
>

Exercise 8

Create the method to_s for your beers so that the text will show both the beer and the brewery name

Modify the form to create ratings. Users should not see the value of the name field of the beers they will choose from; instead, they should find the string of text which is returned by the method to_s of the object.

Exercise 9

Do the same change to the form to create beers (in the file views/beers/_form.html.erb) and to the controller which takes care of showing the form (beers#new). The user should not have to define by hand the brewery of the beer they want to create, but they should be able to choose the brewery out of a list.

Change the controller to create new beers (beers#create), so that the browser is redirected to the list with all beers after a new beer is created. The list address should be generated with a path helper. By default, the browser is redirected to the page of the new beer with the command redirect_to @beer: change this.

The form created automatically by scaffolding contains code for reporting errors, we will get acquainted with this better later.

Exercise 10

By this time, the style of the beers created are given as strings. We will change our application later on so that the beer styles are also stored in the database.

Before we do it, adopt a temporary solution. Change your application so that the style of the beer created can be chosen from a list, which is built based on the table supplied by the controller. The code of the new method of the beer controller will be modified as follows:

Controller

def new
 @beer = Beer.new
 @breweries = Brewery.all
 @styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter", "Low alcohol"]
end

The view will have to generate the selection options of the form based on the @styles table. Instead of using the method options_from_collection_for_select to generate the selection options, in this case you better use the method options_for_select, see http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-options_for_select

Strangely enough, after all these changes you will not be able to edit the beer information. The reason is that the view template which generates the form is the same view template which is used to create a new beer and to edit the beer information (app/views/beers/_form.html.erb). After the changes, the view requires that the variable @breweries contains the brewery list and the variable @styles contains the brewery styles. You access the page to modify beer information after executing the controller method edit, and we will have to modify the controller in the following way to fix the problem:

  def edit
    @breweries = Brewery.all
    @styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter", "Lowalcohol"]
  end

It is very typical, that the controller methods new and edit contain much of the same code. It might make sense to extract the common code and make a new method out of it.

REST and routing

REST (representational state transfer) is an HTTP-protocol-based architecture model which is especially used to implement web-based applications. The idea behind it is simple: the resources for editing and retrieval are defined by addresses, the request methods describe the operation for the resources, and the request body contains the data for the resources, in case they are needed.

Read now http://guides.rubyonrails.org/routing.html till point 2.5. Rails makes it easy to observe a structure such as REST. If you are interested in it, you can read more about REST from here, for instance.

Change the rating paths to the file routes.rb so that we use the ready-made resources definition:

  # comment or remove the old definitions
  #get 'ratings', to: 'ratings#index'
  #get 'ratings/new', to: 'ratings#new'
  #post 'ratings', to: 'ratings#create'

  resources :ratings, only: [:index, :new, :create]

Because you don't need routes like delete, edit and update, use the :only qualifier to choose only the routes you need. Take a look at the paths defined for the application by running the command rails routes from the command line (or from the web page with an erroneous URL):

     ratings GET    /ratings(.:format)            ratings#index
             POST   /ratings(.:format)            ratings#create
  new_rating GET    /ratings/new(.:format)        ratings#new

The result is the same as the one before, but the name of auxiliary method rating_new_path follows now Rails conventions and it's called new_rating_path.

Change the old path method call which is used in the template app/views/ratings/index.erb.html with the new one.

Removing a rating

Add the possibility to remove ratings. First, change routes.rb and add the appropriate route:

resources :ratings, only: [:index, :new, :create, :destroy]

Then add a link in the ratings list to delete a rating:

<ul>
  <% @ratings.each do |rating| %>
    <li> <%= render rating %> <%= button_to 'delete', rating_path(rating.id), method: :delete %> </li>
  <% end %>
</ul>

Rails conventions imply that you delete objects using the HTTP DELETE method. If you want to delete a rating where the ID is 5, clicking the delete button triggers an HTTP DELETE request to the address ratings/5.

As we have mentioned before, the parameter of link_to can be the object which is called instead of the call rating_path(rating.id). The code above can also be shortened to:

<ul>
  <% @ratings.each do |rating| %>
    <li> <%= render rating %> <%= button_to 'delete', rating, method: :delete %> </li>
  <% end %>
</ul>

In order to make deletion work, you have to define the method destroy for the controller, which executes the deletion.

The URL to the method is ratings/[the ID of the object to delete]. According to Rails conventions, the method finds the ID of the object to delete thanks to the params object. The deletion happens when the object is retrieved from the database and its delete method is called:

def destroy
  rating = Rating.find(params[:id])
  rating.delete
  redirect_to ratings_path
end

A redirection will be executed at the end, which leads back to the page with all ratings. Because of the redirection, the browser sends a new GET request to the application for the /ratings address, and the method ratings#index is executed again.

Exercise 11

Deleting ratings has an issue: if users are not careful they might eventually destroy ratings without meaning to do it.

Make it so that users are required to confirm whether they really mean to destroy a rating. See here for help

Orphan objects

If you remove beers with ratings from your application, the ratings which belong to the deleted beer remain in the database. Most likely, this causes that the rating page renders erroneously.

Exercise 12

Remove some beers with ratings and go to the page with all ratings. You will see the error message undefined method `name' for nil:NilClass

The error is caused by the fact that it tries to call beer.name from the method to_s of the rating object.

Delete the orphan ratings by hand from the console. Try to think first of a command/some commands, which can help you to make a list of the orphan ratings. If you can't think of it yourself, you can find a ready-made answer for the exercise below in this page.

The ratings which belong to a beer can be deleted easily automatically. Alongside the beer model code has_many :ratings, you should mark that ratings are dependent on beers, and that they should be destroyed if beers are destroyed:

class Beer < ApplicationRecord
  belongs_to :brewery
  has_many :ratings, dependent: :destroy

  # ...
end

The orphan issue is solved now.

Exercise 13

Implement the same change for the breweries. When a brewery is deleted, its beer should also be deleted.

Create a brewery with at least one beer with ratings. Delete the brewery and make sure, the beers of the brewery and their ratings are deleted.

If you can't yet access individual breweries from the all breweries page, fix it now!

Indirect object connection

Your application is created in a way so that ratings belong to beers and that beers belong to breweries. This means that a set of ratings belong to each brewery, indirectly. Rails provides you with a simple way to go from the breweries to the ratings directly:

class Brewery < ApplicationRecord
  has_many :beers
  has_many :ratings, through: :beers
end

the connection is defined as a "database connection," but it is specified that the connection happens through other beers. Now the brewery has the method ratings which returns the ratings.

Implement this connection in your code and test the following thing from your console (not before doing reload!):

> k = Brewery.find_by name:"Koff"
> k.ratings.count
 => 5

Exercise 14

Change the page which shows the information of the singular breweries so that it tells the amount of ratings of their beers as well as the average value. Add the method average_rating to the brewery to do this.

Make sure the count for the number of ratings is grammatically correct, as it was in exercise 6. If there are no ratings, do not show the average.

The brewery page should be look more or less like the picture below after you have implemented the changes.

picture

You will see, that beer and brewery both a method called average_rating which also works in the same way. You can not leave the code like this.

Moving common code to a module

We notice that beer and brewery both have an identically named method average_rating that also work identically. It is not acceptable to leave our code this way.

Exercise 15

Ruby provides you with a way to share methods between two classes with the help of modules, see https://ruby-doc.com/docs/ProgrammingRuby/html/tut_modules.html

Modules have different uses – forming namespaces, for instance. However, now we are interested in the mixin inheritance which can be implemented with modules.

Get acquainted well enough with modules and refactor your code so that the method average_rating is moved to a module which is contained by the classes Beer and Brewery. As the newly created module is only used by models, it is sensible to define it as a concern and place the file defining it in the directory app/models/concerns

module RatingAverage
 extend ActiveSupport::Concern

 # ...
end
  • Attention: if the name of your module is RatingAverage, exactly like in the example, because of Ruby naming conventions it has to be placed in the file app/models/concerns/rating_average.rb. In fact, even though classes names are PascalCase and start with capital letters, their files names follow the snake_case.rb style.

After you have done the exercise, the class Brewery should look more or less like below (assuming your module is called RatingAverage):

class Brewery < ApplicationRecord
  include RatingAverage

  has_many :beers
  has_many :ratings, through: :beers
end

and the method average_rating should still work like before:

> b = Beer.first
> b.average_rating
=> #<BigDecimal:7fa4bbde7aa8,'0.17E2',9(45)>
> b = Brewery.first
> b.average_rating
=> #<BigDecimal:7fa4bfbf7410,'0.16E2',9(45)>
>

A simple protection

At the end of the week, you now want to make your application in a way so that only the administrator is allowed to delete breweries. In week 3, you will use a more far-reaching way for authentication; implement a shortcut now, using http basic authentication. See http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html

At the same time, you can get to know Rails controllers filter methods, see http://guides.rubyonrails.org/action_controller_overview.html#filters. You can use them to implement easily a particular functionality, so that some particular methods are executed before (before_action) a defined controller method.

We'll define a filter method called authenticate for the brewery controller (which is defined as private). The filter has to be executed before each method of the brewery controller:

class BreweriesController < ApplicationController
  before_action :set_brewery, only: %i[ show edit update destroy ]
  before_action :authenticate

  # ...

  private

  # ...

  def authenticate
    raise "perform authentication"
  end
end

The filter method throws an exception. Therefore, you will be sent an exception every time you go to whatever page of the breweries. Check this out in the browser.

Define the filter method execution so that it affects only when breweries are deleted:

class BreweriesController < ApplicationController
  before_action :set_brewery, only: %i[ show edit update destroy ]
  before_action :authenticate, only: [:destroy]

  # ...

  private

  # ...

  def authenticate
    raise "perform authentication"
  end
end

Check again with your browser that other pages work, but deleting a brewery causes an error.

Implement http-basicauth authentication then (you can read more at http://blog.dcxn.com/2011/09/30/the-simplest-possible-authentication-in-rails-http-auth-basic/)

Hard code an "admin" username with the password "secret":

class BreweriesController < ApplicationController
  before_action :set_brewery, only: [:show, :edit, :update, :destroy]
  before_action :authenticate, only: [:destroy]

  # ...

  private

  # ...

  def authenticate
    authenticate_or_request_with_http_basic do |username, password|
      if username == "admin" and password == "secret"
        return true
      else
        raise "Wrong username or password" # username/password was wrong
      end
    end
  end
end

And your application will work as desired!

ATTENTION: After you have given the right username and password once, the browser will not ask for the ID when you go to the page again. Open a new incognito window, if you want to test the sign-in process again!

The idea behind the method authenticate_or_request_with_http_basic is that the application requests the browser to send a username and password. These are later forwarded to the code block between do and end through the parameters username and password. If the code block is true, the page will be shown to the user.

Because the code block has the same value as the id condition, it can be simplified like this:

def authenticate
  authenticate_or_request_with_http_basic do |username, password|
    raise "Wrong username or password" unless username == "admin" and password == "secret"
    
    return true
  end
end

HTTP Basic authentication is useful for the pages simple protection needs. In more complex situations however, you will have to look for other solutions to provide better data protection.

You'd better note, that the HTTP Basic authentication should not be used for other purposes than the HTTPS protocol, because username and password are sent Base64 encoded. This means that anyone could get to the headers and find out the password. A solution which is slightly better is the Digest access authentication. In this way, users sign in with an identifier which is calculated with a one-way function and not with username and password. Using Digest authentication is simple on Rails, see http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Digest.html

Exercise 16

Expand the solution so that the program would accept other user name-password pairs which are hard coded. The available IDs are hard coded in hashes which are defined in a method. The method has to work with hashes of arbitrary length.

  def authenticate
   admin_accounts = { "pekka" => "beer", "arto" => "foobar", "matti" => "ittam", "vilma" => "kangas" }

   authenticate_or_request_with_http_basic do |username, password|
     # do something here
   end
 end

If you test the functionality, remember that you have to use an incognito browser if you want to sign in again after giving the right combination of username and password already once.

HINT: Coming up with the correct code might be easiest with the help of a debugger. Pause the application execution:

authenticate_or_request_with_http_basic do |username, password|
binding.pry
end

and try what values variables admin_accounts, username and password contain ja form the right command.

HINT 2: The code block should be evaluated either as true or untrue depending on whether the password is correct. The value doesn't however necessarily have to be either true or false because Ruby interprets also other values as either true (truthy) or untrue (falsy). For example nil is interpreted as untrue/falsy. See more at Truth value - Wikipedia, class TrueClass and class FalseClass .

Application to Internet

To end your week, it is time to deploy again your application to either Heroku or Fly.io. Deployment to Fly.io might go without problems as Fly.io automatically executes any database migrations defined in the application. Not so with Heroku.

Problems with Heroku

If you try to navigate to the page with all ratings, you will find the old evil error message:

picture

Tracing down errors in the application running in the production mode is always a bit more difficult than when it is running in the developer mode, where Rails provides the application developer with various ways to find out the problems.

In the production mode, the problems' reason has to be found from the application logs. As we mentioned last week already, you can find heroku application logs with the command heroku logs.

Once more, you'll find the reason of your problems:

> heroku logs
2020-08-20T13:34:55.379420+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Processing by RatingsController#index as HTML
2020-08-20T13:34:55.381470+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rendering ratings/index.html.erb within layouts/application
2020-08-20T13:34:55.384735+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rating Load (1.2ms)  SELECT "ratings".* FROM "ratings"
2020-08-20T13:34:55.385523+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rendered ratings/index.html.erb within layouts/application (3.9ms)
2020-08-20T13:34:55.385780+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Completed 500 Internal Server Error in 6ms (ActiveRecord: 1.2ms)
2020-08-20T13:34:55.386820+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2020-08-20T13:34:55.386846+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] ActionView::Template::Error (PG::UndefinedTable: ERROR:  relation "ratings" does not exist
2020-08-20T13:34:55.386848+00:00 app[web.1]: LINE 1: SELECT "ratings".* FROM "ratings"
2020-08-20T13:34:55.386849+00:00 app[web.1]: ^
2020-08-20T13:34:55.386850+00:00 app[web.1]: : SELECT "ratings".* FROM "ratings"):
2020-08-20T13:34:55.386958+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     1: <h2>List of ratings</h2>
2020-08-20T13:34:55.386960+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     2:
2020-08-20T13:34:55.386966+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     3: <ul>
2020-08-20T13:34:55.386968+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     4:  <% @ratings.each do |rating| %>
2020-08-20T13:34:55.386970+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     5:    <li> <%= rating %> <%= link_to 'delete', rating_path(rating.id), method: :delete, data: { confirm: 'Are you sure?' } %> </li>
2020-08-20T13:34:55.386972+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     6:  <% end %>
2020-08-20T13:34:55.386973+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     7: </ul>
2020-08-20T13:34:55.386977+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2020-08-20T13:34:55.387016+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] app/views/ratings/index.html.erb:4:in `_app_views_ratings_index_html_erb___3457620989041177195_70202650345860'

The ratings database table does not exist. The problem is solved if you execute the migrations:

heroku run rails db:migrate

Next you will generate a situation where your database will be put in a slightly inconsistent condition.

Start heroku console with the command heroku run console and create a beer which does not belong to any brewery

> b = Beer.new name:"crap beer", style:"lager"
> b.save(validate: false)

and create a beer which belongs to a nonexistent brewery (meaning that the reference key brewery ID is erroneous):

> b = Beer.new name:"shitty beer", style:"lager", brewery_id: 123
> b.save(validate: false)

If you go now to the page with all beers, you will find the same unfortunate message "We're sorry, but something went wrong.". Once again, you will find the error by looking into the logs:

2022-08-20T10:56:01.307817+00:00 app[web.1]: F, [2022-08-20T10:56:01.307761 #4] FATAL -- : [22db4647-3122-419e-8e83-e2e99bfe3606]
2022-08-20T10:56:01.307818+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] ActionView::Template::Error (undefined method name' for nil:NilClass):
2022-08-20T10:56:01.307818+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     10:
2022-08-20T10:56:01.307819+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     11:   <p>
2022-08-20T10:56:01.307819+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     12:     <strong>Brewery:</strong>
2022-08-20T10:56:01.307820+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     13:     <%= link_to beer.brewery.name, beer.brewery %>
2022-08-20T10:56:01.307820+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     14:   </p>
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     15:
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     16:   <% if beer.ratings.empty? %>
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/_beer.html.erb:13
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/index.html.erb:7
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/index.html.erb:6

Here is the cause:

undefined method `name' for nil:NilClass

The line which causes our problem is

<%= link_to beer.brewery.name, beer.brewery %>

So there is a beer whose brewery field is nil. This can be because the value of the beer brewery_id is either nil or erroneous (in case of a brewery which has been deleted).

After you have found the reason, you'll have to find the culprit. Open your heroku console with the command heroku run console and retrieve the beers without brewery:

> Beer.all.select{ |b| b.brewery.nil? }
=> [#<Beer id: 8, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2020-08-20 13:37:21", updated_at: "2020-08-20 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2020-08-20 13:38:51", updated_at: "2020-08-20 13:38:51">]
>

The next thing to do is fixing the objects which cause the problem. Because you created them yourself a moment ago for testing reasons, delete the objects (first store into a variable the objects returned by the previous operation which are in the variable _):

> bad_beer = _
=> [#<Beer id: 8, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2020-08-20 13:37:21", updated_at: "2020-08-20 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2020-08-20 13:38:51", updated_at: "2020-08-20 13:38:51">]
> bad_beer.each{ |bad| bad.delete }
> Beer.all.select{ |b| b.brewery.nil? }
=> []
>

Most commonly, the problems we have in production depend on the inconsistent state that some objects have got because of our changes in the database scheme. For instance, they may be belonging to objects which do not exist or the references might be missing. It is a good practice to deploy the application in the production mode as often as possible, in this way, you will know that the potential problems are caused by the changes you have just done and fixing them will be easier.

Because it is a program in production, resetting the database (rails db:drop) is never an acceptable way to "fix" a database inconsistency because the data in production cannot be destroyed. You should start from the beginning to learn reading logs and finding out problems as you are meant to do.

Submitting the exercises

Commit all your changes and push the code to Github. Deploy to the newest version to Heroku or Fly.io, as well.

Mark the exercises you have done at https://studies.cs.helsinki.fi/stats/courses/rails2023/submissions.

And let's continue coding: week 3.