Practical 3

0. Finish Practical Sheet 2. Run the tests (rake test) to make sure everything works fine.

1. Controllers. A controller is responsible for a specific part of an application, and consists of Ruby code that defines methods for the actions in this part of the application. The part that needs to be designed now is how our online store presents itself to a potential buyer, i.e., the catalog. Let’s call the user facing part of the application the store. Let’s make the catalog visible to the user through an index action. An action consists of three things: a route, a controller method and a view template. The command

rails generate controller store index

will create a file store_controller.rb which defines a class StoreController.

Find the file, and describe what’s in there. Then point your browser to

http://localhost:3000/store/index

to see the effect of issuing the index command to the store controller of your application. This works, because the rails generate controller command has set up a new route by adding the line get store/index to the routes in the file config/routes.rb, so that the application now responds to GET store/index HTTP requests.

1a. If all works fine, it might be a good idea to commit the current state of the files that make up the depot application to the repository.

git add .
git commit -m "generated store controller"
git push

2. While you look at the file routes.rb you might as well modify it so that the catalog becomes the home page of our application: replace the line get store/index by the line

root to: 'store#index', as: 'store'

Then reload the page localhost:3000. Can you see the shiny product listing? Why not?

3. In order to properly wire up the store/index action, a few files will need to be modified.

For now, the store controller contains only a skeletal definition of an index method. Add the line

@products = Product.order(:title)

to that method (i.e., before the first end). Here, Product is our old product model, and order is a method that applies to the Product class and creates a list of all products in the database, ordered by their title. This list is assigned to the instance variable @products, so it becomes part of the instance of the controller that deals with store/index requests. Reload localhost:3000. Any visible change? Why not?

4. Recall that an action consists of three things: a route, a controller method and a view template. We’ve set up the route at the beginning of this practical. We have defined the controller method in the previous step. Now, let’s update the view template: replace the store index view template (which file?) by

<% if notice %>
  <aside id="notice"><%= notice %></aside>
<% end %>

<h1>Your Pragmatic Catalog</h1>

<ul class="catalog">
  <% @products.each do |product| %>
    <li>
      <%= image_tag(product.image_url) %>
      <h2><%= product.title %></h2>
      <p><%= sanitize product.description %></p>
      <div class="price"><%= product.price %></div>
    </li>
  <% end %>
</ul>

This mixture of HTML and Ruby (enclosed in <% and %>) essentially sets up a loop over the list @products and displays each product in turn, inside a <li> element of an unordered list (<ul>). How does the browser know how to format these elements? How does the view know about the @products variable?

Reload the page, and now find the catalog.

4a. If all works fine, commit the changes to your local git repository, and push them to the github cloud:

git add .
git commit -m "added a store index action"
git push

5. Still, the catalog could look much nicer. Let’s apply some styling. Open the file app/assets/stylesheets/store.scss (which was automatically generated by the last rails command) and add

.store {
}

to its bottom. This bracket provides a place, where all the style information relating to elements of class store can be collected.

6. Add the following, bit by bit, into that bracket (i.e. between { and }) and reload the catalog page after each line to see its effect.

max-width: 80em;

ul.catalog {
  border-top: solid 0.250em;
  list-style: none;
  padding: 0;
  margin: 0;

  li {
    padding: 1em;
    margin: 0;
    border-bottom: solid thin #ddd;

    &::after {
      clear: both;
      content: " ";
      display: block;
    }

    img {
      float: left;
      padding: 1em;
      margin-right: 1em;
      margin-bottom: 1em;
      box-shadow: 0.176em 0.176em 0.354em 0px rgba(0,0,0,0.75);
    }

    .price {
      font-size: 1.414em;
    }
  }
}

6a. If all looks well, update the repository and push to the cloud.

git commit -a -m "added and styled catalog view"
git push

7. Layouts. A layout is an HTML frame that applies to a whole range of pages. Ours is in app/views/layouts/application.html.erb. It contains the required <html>, <head>, and <body> elements, and sets the title of the browser window. Edit the <title> element in the <head> to look like

<title>Pragprog Books Online Store</title>

If you followed the instructions in Practical 1, and the file contains

<%= content_tag :body, class: controller.controller_name do %>
  <%= yield %>
<% end %>

then replace these 3 lines by

<body>
  <%= content_tag :main, class: controller.controller_name do %>
    <%= yield %>
  <% end %>
</body>

Then add just below the <body> tag,

<header class="main">
  <%= image_tag 'logo.png', alt: 'The Pragmatic Bookshelf' %>
  <h1><%= @page_title %></h1>
</header>

This adds a banner consisting of an image and some text to the top of the page. For this to work as intended, download the file

from http://schmidt.nuigalway.ie/cs424/depot and copy it into the corresponding location inside your own depot application.

8. Rename the file app/assets/stylesheets/application.css to app/assets/stylesheets/application.scss (how?) and add the following to it, bit by bit, and watch the effects:

body {
  margin: 0;
  padding: 0;
}

header.main {
  text-align: center; // center on mobile
  @media (min-width: 30em) {
    text-align: left; // left align on desktop
  }
  background: #282;
  margin: 0;
  h1 {
    display: none;
  }
}

.content {
}

The empty .content { } bracket will be filled in the next step.

9. In the layout file, app/views/layouts/application.html.erb, add the following after the <header> element:

<section class="content">
  <nav class="side_nav">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/questions">Questions</a></li>
      <li><a href="/news">News</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>

and, just before the </body> end-tag:

</section>

This adds a sidebar with a list of (mock) links to the page. In order to have this blend in with the rest, we add some more style to app/assets/stylesheets/application.scss, inside the .content { } bracket:

margin: 0;
padding: 0;

display: flex;
display: -webkit-flex;
flex-direction: column; // mobile is horizontally laid out
-webkit-box-orient: vertical;
-webkit-box-direction: normal;

and:

@media (min-width: 30em) {
  flex-direction: row;  // desktop is vertically laid out
  -webkit-box-orient: horizontal;
}

and:

nav {
  padding-bottom: 1em;
  background: #141;
  text-align: center;  // mobile has centered nav
  @media (min-width: 30em) {
    text-align: left; // desktop nav is left-aligned
    padding: 1em;     // and needs more padding
  }
  ul {
    list-style: none;
    margin: 0;
    padding: 0;
    @media (min-width: 30em) {
      padding-right: 1em; // give desktop some extra space
    }
    li {
      margin: 0;
      padding: 0.5em;
      text-transform: uppercase;
      letter-spacing: 0.354em;
      a {
        color: #bfb;
        text-decoration: none;
      }
      a:hover {
        background: none;
        color: white;
      }
    }
  }
}

and finally:

main {
  padding: 0.5em;
}

Again, if you add this line by line (taking care that all opening braces are always matched by closing ones), you can watch the effects of the individual rules.

10. What’s wrong with the formatting of prices of products? Find the place where they are printed and replace product.price with number_to_currency(product.price) to fix it. Sometimes it can be useful to try out a newly found functionality in a separate environment. The rails console is such a place. It allows you to interact with your application from a command line. Type (in the top depot folder)

rails console

to start it up. The price formatting helper method can be accessed as helper.number_to_currency inside the rails console. Google the documentation of number_to_currency and experiment with its options. How can you make it print four billion euro, with groups of three digits separated by spaces?

10a. If all works fine, commit the changes to your local git repository, and push them to the github cloud.

11. Functional Testing. Functional tests can be used to verify that the model, view and controller work well together. The command assert_select is something like the Swiss army knife for functional testing. It allows you to test properties of specific nodes in the document tree. Add the following four examples of its use to the "should get index" test in the file store_controller_test.rb (which directory?):

assert_select 'nav.side_nav a', minimum: 4
assert_select 'main ul.catalog li', 3
assert_select 'h2', 'Programming Ruby 1.9'
assert_select '.price', /\$[,\d]+\.\d\d/

These tests use CSS selector notation to refer to parts of the HTML that is returned. The first selects all <a> elements which sit inside a <nav> element with attribute class="side_nav" and expects to find at least 4 of them (where?). The remaining tests verify that all products (as specified for test purposes in the file fixtures.yml) are displayed: there should be 3 <li> elements inside an <ul> element with attribute class="catalog", one <h3> element should contain the text 'Book1', and all elements with attribute class="price" should be formatted as dollar prices.

12. Add a date and time to the sidebar. Formulate a test that checks its presence.

12a. If all works fine, commit the changes to your local git repository, and push them to the github cloud.

Any comments or questions? Use the comment box below.

Written on October 28, 2020 by CS424.