[WB//Rails4]

Fortunka v0.0

[Ken Arnold]

Fortunka (Unix) to program „which will display quotes or witticisms. Fun-loving system administrators can add fortune to users’ .login files, so that the users get their dose of wisdom each time they log in.”

Autorem najczęściej instalowanej wersji jest Ken Arnold.

Napisanie za pomocą Ruby on Rails prostej aplikacji CRUD jest proste. Poniżej zademonstruję to na przykładzie aplikacji Fortunka, w której zaimplementujemy interfejs CRUD dla fortunek, czyli krótkich cytatów:

[Agile Programming]

Fortunka krok po kroku

Pierwsza wersja takiej aplikacji to Proverb Hunter. Teraz ta aplikacja rozrosła się do „English Learning Resources”.

Chrome, czyli wygląd, naszej Fortunki przygotujemy korzystając z LessCSS,gemu less-rails, frameworka Bootstrap, less-rails-bootstrap oraz rails-bootstrap-form.

1. Zaczynamy od wygenerowania rusztowania aplikacji i przejścia do katalogu z wygenerowanym rusztowaniem:

rails new my_fortune --skip-bundle --skip-test-unit
cd my_fortune

2. Dopisujemy te gemy do pliku Gemfile:

Gemfile
gem 'therubyracer', '~> 0.12.2'
gem 'less-rails', '~> 2.7.0'
gem 'less-rails-bootstrap', '~> 3.3.4'
gem 'bootstrap_form', '~> 2.3.0'

gem 'faker', '~> 1.4.3'
gem 'quiet_assets', '~> 1.1.0'

i usuwamy z niego gem sass-rails.

Instalujemy gemy i tak jak to opisano w README uruchamiamy generator less_rails_bootstrap:custom_bootstrap:

rails generate less_rails_bootstrap:custom_bootstrap
  create  app/assets/stylesheets/custom_bootstrap/custom_bootstrap.less
  create  app/assets/stylesheets/custom_bootstrap/variables.less
  create  app/assets/stylesheets/custom_bootstrap/mixins.less

i przeklikujemy do plików application.cssapplication.js linijki z require z pliku README powyżej oraz pliku README z gemu bootstrap_form.

Przykładowo do pliku application.css dopisujemy:

*= require custom_bootstrap/custom_bootstrap
*= require rails_bootstrap_forms

3. Dopiero teraz generujemy szablon aplikacji CRUD dla fortunek:

rails generate scaffold fortune quotation:text source:string
rake db:migrate

4. Zmieniamy wygenerowany layout na layout korzystający z frameworka Bootstrap. Skorzystamy z szablonu o nazwie starter template.

W szablonie aplikacji application.html.erb wymieniamy zawartość elementu body na:

app/views/layouts/application.html.erb
<%= render partial: 'shared/navbar' %>
<div class="container">
  <div class="starter-template">
    <%= yield %>
  </div>
</div>

Następnie tworzymy katalog app/views/shared/ w którym tworzymy szablon częściowy o zawartości:

app/views/shared/_navbar.html.erb
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle"
          data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/">My Fortune</a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li class="active"><a href="http://inf.ug.edu.pl">Home</a></li>
        <li><a href="#about">About</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
    </div>
  </div>
</div>

Na koniec dopisujemy na końcu pliku application.css dwie reguły:

app/assets/stylesheets/application.css
body {
  padding-top: 50px;
}
.starter-template {
  padding: 40px 15px;
}

5. Typografia. Kilka poprawek, które zapisujemy w pliku variables.less:

app/assets/stylesheets/custom_bootstrap/variables.less
@text-color: black;
@font-family-sans-serif:  "DejaVu Sans", sans-serif;
@font-family-serif:       "DejaVu Serif", serif;
@font-family-monospace:   "DejaVu Sans Mono", monospace;
@font-family-base:        @font-family-serif;
@font-size-base:          18px;
@line-height-base:        1.44444; // 26/18

gdzie powyżej zwiększamy rozmiar fontu w akapitach aż do 18px, zgodnie z sugestią, że Anything less than 16px is a costly mistake.

6. Zapełniamy bazę jakimiś danymi, dopisując do pliku db/seeds.rb:

db/seeds.rb
Fortune.create! quotation: 'I hear and I forget. I see and I remember. I do and I understand.'
Fortune.create! quotation: 'Everything has its beauty but not everyone sees it.'
Fortune.create! quotation: 'It does not matter how slowly you go so long as you do not stop.'
Fortune.create! quotation: 'Study the past if you would define the future.'

Powyższe fortunki umieszczamy w bazie, wykonujac na konsoli polecenie:

rake db:seed  # load the seed data from db/seeds.rb

Uwaga: Aby wykonać polecenie rake w trybie produkcyjnym poprzedzamy je napisem RAILS_ENV=production, przykładowo:

RAILS_ENV=production rake db:migrate
RAILS_ENV=production rake db:seed

Powyższy kod „smells” (dlaczego?) i należy go poprawić. Na przykład tak jak to zrobiono tutaj seeds.rb.

Jeśli kilka rekordów w bazie to za mało, to możemy do pliku db/seeds.rb wkleić taki kod i ponownie uruchomić powyższe polecenie.

7. Poprawiamy widok index.html.erb. Dopisujemy klasę table do elementu table, klasy btn do odsyłaczy i ustalamy szerokości dwóch pierwszych kolumn tabeli:

<table class="table">
  <thead>
    <tr>
      <th class="col-md-7">Quotation</th>
      <th class="col-md-2">Source</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <% @fortunes.each do |fortune| %>
    <tr>
      <td><%= fortune.quotation %></td>
      <td><%= fortune.source %></td>
      <td><%= link_to 'Show', fortune, class: "btn btn-default btn-sm" %></td>
      <td><%= link_to 'Edit', edit_fortune_path(fortune), class: "btn btn-default btn-sm"  %></td>
      <td><%= link_to 'Destroy', fortune, method: :delete, data: { confirm: 'Are you sure?' },
              class: "btn btn-danger btn-sm" %></td>
    </tr>
    <% end %>
  </tbody>
</table>
<p><%= link_to 'New Fortune', new_fortune_path, class: "btn btn-primary btn-lg" %></p>

8. Zmieniamy widok częściowy _form.html.erb:

<%= bootstrap_form_for(@fortune, layout: :horizontal) do |f| %>
  <%= f.text_area :quotation %>
  <%= f.text_field :source %>
  <%= f.form_group do %>
    <%= f.submit %>
  <% end %>
<% end %>

9. Zmieniamy routing. Ustawiamy stronę startową aplikacji, dopisując w pliku konfiguracyjnym config/routes.rb:

config/routes.rb
Fortunka::Application.routes.draw do
  resources :fortunes
  root to: 'fortunes#index'

Co dalej?

Oczywiście należy poprawić pozostałe widoki.

Różne rzeczy

1. Walidacja, czyli sprawdzanie poprawności (zatwierdzanie) danych wpisanych w formularzach. Przykład, dopisujemy w modelu:

app/models/fortune.rb
validates :quotation, length: {
  minimum: 8,
  maximum: 256
}
validates :source, presence: true

Zobacz też samouczek Active Record Validations.

2. Wirtualne Atrybuty. Przykład: cenę książki pamiętamy w bazie w groszach, ale chcemy ją wypisywać i edytować w złotówkach.

Załóżmy taką schema:

schema.rb
create_table "books", :force => true do |t|
  t.string   "author"
  t.string   "title"
  t.string   "isbn"
  t.integer  "price"
end

Do modelu dopisujemy dwie metody („getter” i „setter”):

book.rb
class Book < ActiveRecord::Base
  def price_pln
    price.to_d / 100 if price
  end
  def price_pln=(pln)
    self.price = pln.to_d * 100 if pln.present?
  end
end

Zamieniamy we wszystkich widokach price na price_pln, przykładowo:

_form.html.erb
<%= f.input :price_pln %>

i zamieniamy price na price_pln w definicji metody book_params w kontrolerze books_controller.rb.

Walidacja wirtualnych atrybutów, zobacz Virtual Attributes.

3. Tagowanie książek.

Instalujemy gem acts-as-taggable-on i instalujemy migracje:

rake acts_as_taggable_on_engine:install:migrations
rake db:migrate

Dopisujemy do modelu Book:

app/models/book.rb
class Book < ActiveRecord::Base
  acts_as_taggable

Przykład do przetestowania na konsoli:

book = Book.find 1
book.tag_list

book.tag_list.add("awesome, slick, hefty")
book.tag_list.remove("awesome")
book.tag_list
book.tag_list.add("awesomer, slicker", parse: true)

book.save

Book.tagged_with "slick"
Book.tagged_with ["slick", "hefty"]

Dodajemy listę tagów do formularza:

_form.html.erb
<%= f.input :tag_list, :label => "Tags (separated by spaces)" %>

a na końcu pliku application.rb dopisujemy:

config/application.rb
ActsAsTaggableOn.delimiter = ' ' # use space as delimiter

Zapisywanie przykładowych danych w bazie

Tutaj przećwiczymy proste zastosowanie gemów FakerPopulator (o ile już działa z Rails 4).

Zaczynamy od wygenerowania rusztowania dla zasobu Friend:

rails g scaffold friend last_name:string first_name:string phone:string motto:text
rake db:migrate

i od „monkey patching” kodu gemu Faker:

faker_pl.rb
module Faker
  class PhoneNumber
    SIMPLE_FORMATS  = ['+48 58-###-###-###', '(58) ### ### ###']
    MOBILE_FORMATS  = ['(+48) ###-###-###', '###-###-###']

    def self.pl_phone_number(kind = :simple)
      Faker::Base.numerify const_get("#{kind.to_s.upcase}_FORMATS").sample
    end
  end
end

(zob. też ten gist).

Sprawdzamy jak to działa na konsoli:

irb

gdzie wpisujemy:

require 'faker'
require './faker_pl'
Faker::PhoneNumber.pl_phone_number :mobile
Faker::Name.first_name
Faker::Name.last_name

Jeśli wszystko działa tak jak powinno, to w pliku db/seeds.rb możemy wpisać:

db/seeds.rb
require Rails.root.join('db', 'faker_pl')

Friend.populate(100..200) do |friend|
  friend.first_name = Faker::Name.first_name
  friend.last_name = Faker::Name.last_name
  friend.phone = Faker::PhoneNumber.pl_phone_number :mobile
  friend.motto = Populator.sentences(1..2)
end

Teraz wykonujemy:

rake db:seed

zapełniając tabelę friends danymi testowymi.

Chociaż przydałoby się dodać do powyższego kodu coś w stylu:

Friend.populate(1000..5000) do |friend|
  # passing array of values will randomly select one
  friend.motto = ["akapity", "z kilku", "fajnych książek"]
end

I jeszcze dwie uwagi

1. Potrzebne nam gemy wyszukujemy na The Ruby Toolbox. Tam też sprawdzamy, czy gem jest aktywnie rozwijany, czy będzie działał z innymi gemami i wersjami Ruby, itp.

2. Modyfikujemy domyślne ustawienia konsoli Ruby (i równocześnie konsoli Rails):

~/.irbrc
require 'irb/completion'
require 'irb/ext/save-history'

IRB.conf[:SAVE_HISTORY] = 1000
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb_history"
IRB.conf[:PROMPT_MODE] = :SIMPLE

# remove the SQL logging
# ActiveRecord::Base.logger.level = 1 if defined? ActiveRecord::Base

# add hirb gem to Gemfile
if defined? Rails
  begin
    require 'hirb'
    Hirb.enable
  rescue LoadError
  end
end