[WB//Rails4]

Fortunka v1.0

[nowy projekt]

New project car

Jednym ze sprawdzonych sposobów kontynuacji projektu jest rozpoczęcie go od nowa.

TODO

Tym razem nie użyjemy generatora „scaffold”. Skorzystamy z generatora responders_controller z gemu responders.

Co na tym zyskujemy opisano tutaj:

Ale rusztowanie aplikacji wygenerujemy za pomocą szablonu aplikacji Rails:

rails new fortune-responders-4.x \
  -m https://raw.github.com/wbzyl/rails4-tutorial/master/lib/doc/app_templates/wbzyl-template-rails4.rb

Link do repozytorium z kodem fortunki fortune-responders-4.x.

Robótki ręczne…

Dopisujemy gemy responders i will_paginate do pliku Gemfile:

Gemfile
gem 'responders', git: 'git://github.com/plataformatec/responders.git'
gem 'will_paginate', git: 'git://github.com/mislav/will_paginate.git'

Korzystamy z ostatnich wersji gemów z repozytoriów. Te wersje powinny działać z Rails 4.0.0.rc1.

[Fraunhofer lines]

[Herschel do Babbage’a, kiedy ten nie był w stanie dojrzeć ciemnych linii Fraunhofera] Często nie widzimy czegoś dlatego, że nie wiemy jak to zobaczyć, a nie na skutek jakichś braków w organie widzenia… Nauczę cię jak je dostrzec.

Instalacja i instalacja następcza

… czyli install i post install:

bundle install
rails generate responders:install
  create  lib/application_responder.rb
 prepend  app/controllers/application_controller.rb
  insert  app/controllers/application_controller.rb
  create  config/locales/responders.en.yml

Jak widać powyżej, generator responders:install dodał kilka plików i dopisał coś do pliku application_controller.rb.

application_responder.rb:

lib/application_responder.rb
class ApplicationResponder < ActionController::Responder
  include Responders::FlashResponder
  include Responders::HttpCacheResponder
  # Uncomment this responder if you want your resources to redirect to the collection
  # path (index action) instead of the resource path for POST/PUT/DELETE requests.
  # include Responders::CollectionResponder
end

Google podpowiada, że responder to hiszpańskie słowo na odpowiadać / odezwać / odczytać. Jak widać w kodzie powyżej mamy do dyspozycji trzy respondery:

app/controllers/application_controller.rb:

app/controllers/application_controller.rb
require "application_responder"
class ApplicationController < ActionController::Base
  self.responder = ApplicationResponder
  respond_to :html
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
end

responders.en.yml:

config/locales/responders.en.yml
en:
  flash:
    actions:
      create:
        notice: '%{resource_name} was successfully created.'
        # alert: '%{resource_name} could not be created.'
      update:
        notice: '%{resource_name} was successfully updated.'
        # alert: '%{resource_name} could not be updated.'
      destroy:
        notice: '%{resource_name} was successfully destroyed.'
        alert: '%{resource_name} could not be destroyed.'

Generujemy navbar:

rails generate bootstrap:partial navbar
    create  app/views/shared/_navbar.html.erb

dodajemy go do layoutu aplikacji i przy okazji poprawiamy go.

Co to jest responders controller?

Oto odpowiedź:

rails generate responders_controller
Usage:
  rails generate responders_controller NAME [field:type field:type] [options]
  ...
Description:
    Stubs out a scaffolded controller and its views. Different from rails
    scaffold_controller, it uses respond_with instead of respond_to blocks.
    Pass the model name, either CamelCased or under_scored. The controller
    name is retrieved as a pluralized version of the model name.

Generujemy kontroler:

rails generate responders_controller Fortune quotation:text source:string
  create  app/controllers/fortunes_controller.rb
  invoke  erb
  create    app/views/fortunes
  create    app/views/fortunes/index.html.erb
  create    app/views/fortunes/edit.html.erb
  create    app/views/fortunes/show.html.erb
  create    app/views/fortunes/new.html.erb
  create    app/views/fortunes/_form.html.erb
  ...

responder controller definiuje siedem metod:

Oto kod wygenerowanego kontrolera:

app/controllers/fortunes_controller.rb
class FortunesController < ApplicationController
  def index
    @fortunes = Fortune.all
    respond_with(@fortunes)
  end
  def show
    @fortune = Fortune.find(params[:id])
    respond_with(@fortune)
  end
  def new
    @fortune = Fortune.new
    respond_with(@fortune)
  end
  def edit
    @fortune = Fortune.find(params[:id])
  end
  def create
    @fortune = Fortune.new(params[:fortune])
    @fortune.save
    respond_with(@fortune)
  end
  def update
    @fortune = Fortune.find(params[:id])
    @fortune.update(params[:fortune])
    respond_with(@fortune)
  end
  def destroy
    @fortune = Fortune.find(params[:id])
    @fortune.destroy
    respond_with(@fortune)
  end
end

Jak widać wygenerowany kontroler nie korzysta ze strong parameters. Dopisujemy brakujący kod i poprawiamy co trzeba. Oto kod po poprawkach:

app/controllers/fortunes_controller.rb
class FortunesController < ApplicationController
  before_action :set_fortune, only: [:show, :edit, :update, :destroy]
  def index
    @fortunes = Fortune.all
    respond_with(@fortunes)
  end
  def show
    respond_with(@fortune)
  end
  def new
    @fortune = Fortune.new
    respond_with(@fortune)
  end
  def edit
  end
  def create
    @fortune = Fortune.new(fortune_params)
    @fortune.save
    respond_with(@fortune)
  end
  def update
    @fortune.update(fortune_params)
    respond_with(@fortune)
  end
  def destroy
    @fortune.destroy
    respond_with(@fortune)
  end
private
  # Use callbacks to share common setup or constraints between actions.
  def set_fortune
    @fortune = Fortune.find(params[:id])
  end
  # Never trust parameters from the scary internet, only allow the white list through.
  def fortune_params
    params.require(:fortune).permit(:quotation, :source)
  end
end

Pozostał wygenerować model Fortune, migracja modelu:

rails generate model Fortune quotation:text source:string
rake db:migrate

i zapisanie w bazie jakiś danych testowych:

rake db:seed

Tagujemy tę wersję:

git tag v0.1

Dochodząc do v1.0

Zaczynamy od najłatwiejszej rzeczy – paginacji. Przeglądają gemy na stronie Ruby Toolbox widzimy, że tak naprawdę mamy tylko jedną opcję:

Zmiany w kodzie będziemy wprowadzać na osobnej gałęzi:

git checkout -b pagination

Podmieniamy w kodzie metody index kontrolera FortunesController:

@fortunes = Fortune.order('created_at DESC').page(params[:page]).per_page(4)

Dopisujemy do widoku index:

app/views/fortunes/index.html.erb
<div class="digg_pagination">
  <div class="page_info">
    <%= page_entries_info @fortunes %>
  </div>
  <%= will_paginate @fortunes, :container => false %>
</div>

Stylizacja. Korzystamy z gotowego kodu:

Pobieramy plik i zapisujemy go w katalogu: app/assets/stylesheets. Dopisujemy go do manifestu application.css.less:

@import "twitter/bootstrap";
@import "digg_pagination";

Zrobione? Zrobione!

Zabieramy się za i18n:

Dopisujemy do pliku en.yml:

en:
  will_paginate:
    previous_label: "«"
    next_label: "»"
    page_gap: ""
    page_entries_info:
      single_page:
        zero:  "No %{model} found"
        one:   "Displaying 1 %{model}"
        other: "Displaying all %{count} %{model}"
      single_page_html:
        zero:  "No %{model} found"
        one:   "Displaying <b>1</b> %{model}"
        other: "Displaying <b>all&nbsp;%{count}</b> %{model}"
      multi_page: "Displaying %{model} %{from} - %{to} of %{count} in total"
      multi_page_html: "Displaying %{model} <b>%{from} – %{to}</b> of <b>%{count}</b> in total"

(Zamieniam previous, next i page na «, » i …, odpowiednio.)

Wykonujemy commit i na gałęzi master scalamy zmiany i tagujemy tę wersję:

git merge pagination
git tag v0.2

Walidacja

Do czego jest nam potrzebna walidacja wyjaśniono w samouczku Active Record Validations:

Przy okazji warto też przejrzeć samouczek Active Record Callbacks.

Dopisujemy walidację do modelu Fortune:

app/models/fortune.rb
class Fortune < ActiveRecord::Base
  validates :quotation, presence: true
  validates :quotation, length: { maximum: 256 }
  validates :quotation, uniqueness: { case_sensitive: false }

  validates :source, length: { in: 3..64 }, allow_blank: true
end

Sprawdzamy na konsoli Rails jak działa walidacja:

f = Fortune.new
f.valid?                   #=> false
f.errors.messages          #=> {:quotation=>["can't be blank"]}
f.errors[:quotation].any?  #=> true
f.save                     #=> false
f.source = "a"
f.valid?
f.errors.messages
f.source = ""
f.valid?
f.errors

Pozostałe rzeczy: validates_with, validates_each, walidacja warunkowa, walidacja dla powiązanego modelu validates_associated, opcja :on – kiedy walidować (:create, :update, :save itd.) wyjaśniono w samouczkach.

Grand refactoring

W widoku index wykorzystamy tzw. implicit loop.

Wycinamy kod z pętlą po @fortunes z pliku index.html.erb:

app/views/fortunes/index.html.erb
<% @fortunes.each do |fortune| %>
  <tr>
    <td class='span1'><%= link_to fortune.id, fortune_path(fortune) %></td>
    <td class='span6'><%= fortune.quotation %></td>
    <td class='span3'><%= fortune.source %></td>
    <td class='span2'>
      <%= link_to t('.edit', :default => t("helpers.links.edit")),
                  edit_fortune_path(fortune), :class => 'btn btn-mini' %>
      <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
                  fortune_path(fortune),
                  :method => :delete,
                  :data => { :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')) },
                  :class => 'btn btn-mini btn-danger' %>
    </td>
  </tr>
<% end %>

i zastępujemy go odwołaniem do widoku częściowego:

app/views/fortunes/index.html.erb
<%= render partial: 'fortune', collection: @fortunes %>

Wycięty kod wklejamy do pliku _fortune.html.erb i usuwamy pierwszy i ostatni wiersz (pętlę).

Szablon częściowy _fortune.html.erb renderowany jest wielokrotnie w pętli (implicit loop) po zmiennej fortune. Korzystamy z konwencji l.mn. → l.poj. (tutaj @fortunes → fortune).

Jeśli wszystko działa, to scalamy zmiany i tagujemy:

git tag v0.4

Wyszukiwanie w fortunkach

Na stronie z listą fortunek dodamy formularz, który będzie filtrował dane po polu quotation:

app/views/fortunes/index.html.erb
<%= form_tag fortunes_path, method: :get, id: "fortunes_search", class: "form-inline" do %>
  <%= text_field_tag :query, params[:query], class: "span4" %>
  <%= submit_tag "Search", name: nil, class: "btn" %>
<% end %>

Aby odfiltrować zbędne rekordy, musimy w FortunesController w metodzie index przekazać tekst, który wpisano w formularzu.

W tym celu użyjemy nowej metody text_search:

app/controllers/fortunes_controller.rb
def index
  @fortunes = Fortune.text_search(params[:query])
    .order('created_at DESC')
    .page(params[:page])
    .per_page(4)
  respond_with(@fortunes)
end

Kod metody wpisujemy w klasie Fortune:

app/models/fortune.rb
def self.text_search(query)
  if query.present?
    # SQLite
    where('quotation like ?', "%#{query}%")
    # PostgreSQL; i – ignore case
    # where("quotation ilike :q or source ilike :q", q: "%#{query}%")
  else
    all
  end
end

Metoda text_search jest wykonywana zawsze po wejściu na stronę index, nawet jak nic nie wyszukujemy. Prześledzić działanie search? Jak? Co oznacza scoped?

Komentarze do fortunek

[surgery.com]

Software is invisible and unvisualizable. Geometric abstractions are powerful tools. The floor plan of a building helps both architect and client evaluate spaces, traffic flows, views. Contradictions and omissions become obvious.

In spite of progress in restricting and simplifying the structures of software, they remain inherently unvisualizable, and thus do not permit the mind to use some of its most powerful conceptual tools. This lack not only impedes the process of design within one mind, it severely hinders communication among minds.

— Frederick P. Brooks, Jr.

W widoku show.html.erb fortunki chcielibyśmy mieć możliwość dopisywania własnych komentarzy.

Dodatkowo przygotujemy stronę ze wszystkimi komentarzami, gdzie będzie można usuwać i edtować komentarze.

Jak zwykle nowy kod będziemy wpisywać na nowej gałęzi:

git checkout -b comments

Dopiero po sprawdzeniu, że kod jest OK, przeniesiemy go na gałąź master.

Zaczynamy od wygenerowania rusztowania dla zasobu Comment:

rails g resource Comment fortune:references \
    body:string author:string

  invoke  active_record
  create    db/migrate/..._create_comments.rb
  create    app/models/comment.rb
  invoke    rspec
  create      spec/models/comment_spec.rb
  invoke  controller
  create    app/controllers/comments_controller.rb
  invoke    erb
  create      app/views/comments

rake db:migrate

Ponieważ fortunka może mieć wiele komentarzy, zagnieżdżamy zasoby w tej kolejności:

resources :fortunes do
  resources :comments
end

Sprawdzamy jak wygląda routing po tej zmianie:

rake routes

(poniżej usunięto (.:format) w URI Pattern)

              Prefix Verb   URI Pattern                             Controller#Action
                root GET    /                                       fortunes#index
    fortune_comments GET    /fortunes/:fortune_id/comments          comments#index
                     POST   /fortunes/:fortune_id/comments          comments#create
 new_fortune_comment GET    /fortunes/:fortune_id/comments/new      comments#new
edit_fortune_comment GET    /fortunes/:fortune_id/comments/:id/edit comments#edit
     fortune_comment GET    /fortunes/:fortune_id/comments/:id      comments#show
                     PATCH  /fortunes/:fortune_id/comments/:id      comments#update
                     PUT    /fortunes/:fortune_id/comments/:id      comments#update
                     DELETE /fortunes/:fortune_id/comments/:id      comments#destroy
            fortunes GET    /fortunes                               fortunes#index
                     POST   /fortunes                               fortunes#create
         new_fortune GET    /fortunes/new                           fortunes#new
        edit_fortune GET    /fortunes/:id/edit                      fortunes#edit
             fortune GET    /fortunes/:id                           fortunes#show
                     PATCH  /fortunes/:id                           fortunes#update
                     PUT    /fortunes/:id                           fortunes#update
                     DELETE /fortunes/:id                           fortunes#destroy

Dla porównania stary routing:

      Prefix Verb   URI Pattern                  Controller#Action
        root GET    /                            fortunes#index
    fortunes GET    /fortunes(.:format)          fortunes#index
             POST   /fortunes(.:format)          fortunes#create
 new_fortune GET    /fortunes/new(.:format)      fortunes#new
edit_fortune GET    /fortunes/:id/edit(.:format) fortunes#edit
     fortune GET    /fortunes/:id(.:format)      fortunes#show
             PATCH  /fortunes/:id(.:format)      fortunes#update
             PUT    /fortunes/:id(.:format)      fortunes#update
             DELETE /fortunes/:id(.:format)      fortunes#destroy

Przechodzimy do modelu Comment, gdzie znajdujemy dopisane przez generator powiązanie:

app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :fortune
end

Przechodzimy do modelu Fortune, gdzie sami dopisujemy drugą stronę powiązania:

app/models/fortune.rb
class Fortune < ActiveRecord::Base
  has_many :comments, dependent: :destroy
  ...

Modele na konsoli Rails

Wchodzimy na konsolę Rails (sandbox):

rails console --sandbox

Na konsoli wykonujemy:

Fortune.last.comments  #=> []

czyli komentarze pierwszej fortunki tworzą pustą tablicę. Aby dodać komentarz możemy postąpić tak:

Fortune.last.comments << Comment.new(author: "Ja", body: "Fajne!")
Comment.all

Gdzie będziemy wypisywać komentarze?

Komentarze będziemy wypisywać tylko dla fortunki do której zostały dodane.

Dlatego dodamy je do widoku fortunes/show:

app/views/fortunes/show.html.erb
<%- model_class = Comment -%>

<% if @fortune.comments.any? %>
  <h2>Comments</h2>
  <% @fortune.comments.each do |comment| %>
  <dl class="dl-horizontal">
    <dt><%= model_class.human_attribute_name(:body) %>:</strong></dt>
    <dd><%= comment.body %></dd>
    <dt><strong><%= model_class.human_attribute_name(:author) %>:</strong></dt>
    <dd><%= comment.author %></dd>
  </dl>
  <div class="form-actions">
    <%= link_to t('.edit', :default => t("helpers.links.edit")),
       edit_fortune_comment_path(@fortune, comment), class: 'btn' %>
    <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
       [@fortune, comment], method: :delete, class: 'btn btn-danger'%>
  </div>
  <% end %>
<% end %>

A link do zagnieżdżonego zasobu comments:

DELETE /fortunes/:fortune_id/comments/:id(.:format)  comments#destroy

tworzymy w taki sposób:

link_to 'Delete', [@fortune, comment], method: :delete

(Link jeszcze nie działa!)

Gdzie będziemy dodawać nowe komentarze?

Najwygodniej jest dodawać komentarze w widoku show:

app/views/fortunes/show.html.erb
<%= simple_form_for [@fortune, @comment], :html => { :class => 'form-horizontal' } do |f| %>

<% if f.error_notification %>
<div class="alert alert-error fade in">
  <a class="close" data-dismiss="alert" href="#">&times;</a>
  <%= f.error_notification %>
</div>
<% end %>

<%= f.input :body, :input_html => { :class => "span6" } %>
<%= f.input :author, :input_html => { :class => "span6" } %>

<div class="form-actions">
  <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
          fortunes_path, :class => 'btn' %>
  <%= f.button :submit, :class => 'btn btn-primary' %>
</div>
<% end %>

Aby dodawanie komentarzy działało musimy zdefiniować zmienną @commentFortunesController:

app/controllers/fortunes_controller.rb
def show
  @comment = Comment.new
  respond_with(@fortune)
end

Kontroler dla komentarzy

W utworzonym przez generator pustym kontrolerze CommentsController wklepujemy poniższy kod:

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_filter do
    @fortune = Fortune.find(params[:fortune_id])
  end

  # POST /fortunes/:fortune_id/comments
  def create
    @comment = @fortune.comments.build(comment_params)
    @comment.save
    respond_with(@fortune, @comment, location: @fortune)
    # uwaga! taki kod nie działa:
    # respond_with([@fortune, @comment], location: @fortune)
  end

  # DELETE /fortunes/:fortune_id/comments/:id
  def destroy
    @comment = @fortune.comments.find(params[:id])
    @comment.destroy
    respond_with(@fortune, @comment, location: @fortune)
    # uwaga! jw.
  end

private
  def comment_params
    params.require(:comment).permit(:body, :author)
  end
end

W powyższym kodzie wymuszamy za pomocą konstrukcji z location, przeładowanie strony. Po zapisaniu w bazie nowej fortunki lub po jej usunięciu nastąpi przekierowywanie do widoku show dla fortunki a nie do widoku show (którego nie ma) dla komentarzy!

To jeszcze nie wszystko. Musimy napisać metody edit oraz update. Ale to zrobimy później. Teraz zabierzemy się za refaktoryzację kodu.

Refaktoryzacja widoku „show”

Usuwamy kod formularza wpisany pod znacznikiem Add new comment. Z usuniętego kodu tworzymy szablon częściowy comments/_form.html.erb.

W widoku show, zamiast usuniętego kodu wpisujemy:

app/views/fortunes/show.html.erb
<h2>Add new comment</h2>
<%= render partial: 'comments/form' %>

Następnie usuwamy z widoku pętlę pod Comments. Z ciała pętli tworzymy drugi szablon częściowy comments/_comment.html.erb:

app/views/comments/_comment.html.erb
<%- model_class = Comment -%>

<dl class="dl-horizontal">
  <dt><%= model_class.human_attribute_name(:body) %>:</strong></dt>
  <dd><%= comment.body %></dd>
  <dt><strong><%= model_class.human_attribute_name(:author) %>:</strong></dt>
  <dd><%= comment.author %></dd>
</dl>
<div class="form-actions">
  <%= link_to t('.edit', :default => t("helpers.links.edit")),
     edit_fortune_comment_path(@fortune, comment), class: 'btn' %>
  <%= link_to t('.destroy', :default => t("helpers.links.destroy")),
    [@fortune, comment], method: :delete, class: 'btn btn-danger'%>
</div>

W widoku, zamiast usuniętego kodu wpisujemy:

app/views/fortunes/show.html.erb
<% if @fortune.comments.any? %>
<h2>Comments</h2>
<%= render partial: 'comments/comment', collection: @fortune.comments %>
<% end %>

Reszta obiecanego kodu

…czyli kod metod edit i update:

app/controllers/comments_controller.rb
# GET /fortunes/:fortune_id/comments/:id/edit
def edit
  @comment = @fortune.comments.find(params[:id])
end

# PUT /fortunes/:fortune_id/comments/:id
def update
  @comment = @fortune.comments.find(params[:id])
  @comment.update(comment_params)
  respond_with(@fortune, @comment, location: @fortune)
end

oraz szablon widoku – comments/edit.html.erb:

app/views/comments/edit.html.erb
<%- model_class = Comment -%>
<div class="page-header">
  <h1><%=t '.title', :default => [:'helpers.titles.edit', 'Edit %{model}'],
    :model => model_class.model_name.human %></h1>
</div>
<%= render :partial => 'form' %>

Walidacja komentarzy

Będziemy wymagać, aby każde pole było niepuste:

app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :fortune
  validates :author, presence: true
  validates :body, presence: true
end

Engines are back in Rails 3.1+

Engine to aplikacja Rails zaprogramowana jako gem. Na przykład gem Devise implementuje autentykację, a gem Kaminari – paginację.

Engines dla Rails wymyślił James Adams. Z engines był jeden problem. Nie było gwarancji, że nowa wersja Rails będzie będzie działać z engines napisanymi dla wcześniejszych wersji Rails. Wersja Rails 3.1 to zmienia.

Nieco Railsowej archeologii:

Korzystając z Devise dodać autentykację do Fortunki.

[source: http://diveintohtml5.org/]
(źródło M. Pilgrim. Dive into HTML5)

TODOs

CSRF and Rails

XSS and Rails

— Neeraj Singh

Atrakcyjność Fortunki możemy zwiększyć implementując coś rzeczy wypisanych poniżej:

Zmieniając widok strony głównej aplikacji, na przykład tak:

Dodając obrazki do fortunek, coś w stylu Demotywatorów lub Kwejka. Można skorzystać z gemu Carrierwave:

lub z gemu Paperclip.

TODO: Uaktualnić przykład library-carrierwave (Bitbucket) do Rails 4.0.x.

Tagowanie

TODO: Zobacz Milestones.

Tagowanie dodamy, korzystając z gemu acts-as-taggable-on. Po dopisaniu gemu do pliku Gemfile:

Gemfile
gem 'acts-as-taggable-on', '~> 2.2.2'

instalujemy go i dalej postępujemy zgodnie z instrukcją z README:

bundle install
rails generate acts_as_taggable_on:migration
rake db:migrate

Warto przyjrzeć się wygenerowanej migracji:

class ActsAsTaggableOnMigration < ActiveRecord::Migration
  def self.up
    create_table :tags do |t|
      t.string :name
    end
    create_table :taggings do |t|
      t.references :tag
      # You should make sure that the column created is
      # long enough to store the required class names.
      t.references :taggable, :polymorphic => true
      t.references :tagger, :polymorphic => true
      # limit is created to prevent mysql error o index lenght for myisam table type.
      # http://bit.ly/vgW2Ql
      t.string :context, :limit => 128
      t.datetime :created_at
    end
    add_index :taggings, :tag_id
    add_index :taggings, [:taggable_id, :taggable_type, :context]
  end

  def self.down
    drop_table :taggings
    drop_table :tags
  end
end

A little inaccuracy saves a world of explanation.

Polimorficzne powiązanie oznacza, że jeden model może być w relacji belongs_to do więcej niż jednego modelu:

t.references :taggable, :polymorphic => true

co rozwija się do takiego kodu:

t.integer :taggable_id
t.string  :taggable_type

który możemy tak zinterpretować:

Zmiany w kodzie modelu

Dopisujemy do modelu:

app/models/fortune.rb
class Fortune < ActiveRecord::Base
  acts_as_taggable_on :tags
  ActsAsTaggableOn::TagList.delimiter = " "

Przy okazji, zmieniamy z przecinka na spację domyślny znak oddzielający tagi.

Po tych zmianach przyjrzymy się bliżej polimorfizowi na konsoli:

f = Fortune.find 1
f.tag_list = "everything nothing always"            # proxy
# f.tag_list = ['everything', 'nothing', 'always']  # tak też można
f.save
f.tags
f.taggings

W widoku częściowym _form.html.erb dopisujemy:

app/views/fortunes/_form.html.erb
<%= f.input :tag_list, :input_html => {:size => 40} %>

A w widoku częściowym _fortune.html.erb oraz w widoku show.html.erb (2 razy) dopisujemy:

app/views/fortunes/_fortune.html.erb
<p><i>Tags:</i> <%= @fortune.tag_list %></p>

Dodajemy losowe tagi do fortunek

Poprawiamy seeds.rb:

db/seeds.rb
platitudes = File.readlines(Rails.root.join('db', 'platitudes.u8'), "\n%\n")
tags = ['always', 'always', 'sometimes', 'never', 'maybe', 'ouch', 'wow', 'nice', 'wonderful']
platitudes.map do |p|
  reg = /\t?(.+)\n\t\t--\s*(.*)\n%\n/m
  m = p.match(reg)
  if m
    f = Fortune.new :quotation => m[1], :source => m[2]
  else
    f = Fortune.new :quotation => p[0..-4], :source => Faker::Name.name
  end
  f.tag_list = tags.sample(rand(tags.size - 3))
  f.save
end

Teraz, kasujemy bazę i wrzucamy jeszcze raz cytaty, ale tym razem z tagami:

rake db:drop
rake db:setup

Chmurka tagów

Jak samemu wygenerować chmurkę tagów opisał Jason Davies, Word Cloud Generator.

Aby wyrenderować chmurkę tagów – niestety nie tak ładną jak ta:

[chmurka tagów]

postępujemy tak jak to opisano w README w sekcji „Tag cloud calculations”:

app/views/fortunes/index.html.erb
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
  <%= link_to tag.name, LINK_DO_CZEGO?, :class => css_class %>
<% end %>

Aby ten kod zadziałał musimy zdefiniować zmienną @tags, wczytać kod metody pomocniczej tag_cloud, wystylizować chmurkę tagów oraz podmienić LINK_DO_CZEGO? na coś sensownego.

Zaczniemy od zdefiniowania zmiennej @tags:

app/controllers/fortunes_controller.rb
def index
  @fortunes = ... bez zmian ...
  @tags = Fortune.tag_counts
  respond_with(@fortunes)
end

Teraz spróbujemy przyjrzeć się bliżej zmiennej tag. W tym celu skorzystamy z metody pomocniczej debug (na razie zamiast LINK_DO_CZEGO? wpiszemy fortunes_path:

app/views/fortunes/index.html.erb
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
  <%= link_to tag.name, fortunes_path, :class => css_class %>
  <%= debug(tag.class) %>
  <%= debug(tag) %>
<% end %>

Po ponownym wczytaniu strony fortunes#index widzimy, że zmienna tag, to obiekt klasy:

ActsAsTaggableOn::Tag(id: integer, name: string)

na przykład:

attributes:
  id: 1
  name: sometimes
  count: 151

Tagi powinny mieć wielkość zależną od częstości ich występowania:

public/stylesheets/application.css
.css1 { font-size: 1.0em; }
.css2 { font-size: 1.2em; }
.css3 { font-size: 1.4em; }
.css4 { font-size: 1.6em; }

Tyle debuggowania – usuwamy zbędny kod z debug. Opakowujemy tag_cloud elementem div#tag_cloud, ustawiamy jego szerokość, powiedzmy na 250px i pozycjonujemy abolutnie w prawym górnym rogu, gdzie jest trochę wolnego miejsca:

public/stylesheets/application.css
#tag-cloud {
  background-color: #E1F5C4; /* jasnozielony */
  margin-top: 1em;
  margin-bottom: 1em;
  padding: 1em;
  width: 100%;
}

I już możemy obejrzeć rezultaty naszej pracy!

Powinniśmy jeszcze dopisać do widoku częściowego _fortune.html.erb kod wypisujący tagi:

app/views/fortunes/_fortune.html.erb
<div class="attribute">
  <span class="name"><%= t "simple_form.labels.fortune.source" %></span>
  <span class="value tags"><%= fortune.tag_list.join(", ") %>
  </span>
</div>
[word cloud: REST on wikipedia]

Dodajemy własną akcję do REST

Mając chmurkę z tagami, wypadałoby olinkować tagi tak, aby po kliknięciu na nazwę wyświetliły się fortunki otagowane tą nazwą.

Zaczniemy od zmian w routingu. Usuwamy wiersz:

config/routes.rb
resources :fortunes

Zamiast niego wklejamy:

config/routes.rb
resources :fortunes do
  collection do
    get :tags
  end
end

Sprawdzamy co się zmieniło w routingu:

rake routes

i widzimy, że mamy jeden dodatkowy uri:

tags_fortunes GET /fortunes/tags(.:format) {:action=>"tags", :controller=>"fortunes"}

Na koniec, zamieniamy link fortunes_path w chmurce tagów na:

<%= link_to tag.name, tags_fortunes_path(name: tag.name), class: css_class %>

Pozostała refaktoryzacja @tags, oraz napisanie metody tags:

app/controllers/fortunes_controller.rb
before_filter only: [:index, :tags] do
  @tags = Fortune.tag_counts
end

# respond_with nic nie wie o tags
def tags
  @fortunes = Fortune.tagged_with(params[:name])
    .page(params[:page]).per_page(4)
  respond_with(@fortunes) do |format|
    format.html { render action: 'index' }
  end
end

def index
  @fortunes = Fortune.text_search(params[:query])
    .page(params[:page]).per_page(4)
  respond_with(@fortunes)
end