Blokowe helpery w Ruby on Rails

08 lipca 2008, 10:28:11

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 4 komentarze

Bardzo przydatną rzeczą, którą możemy zrobić podczas refaktoryzacji kodu widoków jest dopisanie helperów akceptujących bloki kodu.

W Railsach standardowo mamy co najmniej kilka takich helperów, m.in. form_for, czy link_to (póki co w wersji EDGE). Nic nie stoi na przeszkodzie, aby dopisać swoje własne metody akceptujące bloki, które w widoczny sposób poprawiają czytelność kodu.

Jako przykład podam implementację rounded-box jako helper akceptujący blok. Końcowy efekt wyglądał będzie następująco:

<%- rounded_box do %>
Treść, która powinna znaleźć się w boksie
<% end -%>

Napisanie tego typu metod jest bardzo proste i opiera się na wykorzystaniu metody concat. Zakładam, że cały kod naszego boksu na postać (styl CSS zostawmy w spokoju):

<div class="rnd_container">
  <b class="rnd_top">
    <b class="rnd_b1"></b>
    <b class="rnd_b2"></b>
    <b class="rnd_b3"></b>
    <b class="rnd_b4"></b>
  </b>
<div class="rnd_content"> 
  <h2><p>Treść w boksie</p></h2>
</div>
  <b class="rnd_bottom">
    <b class="rnd_b4"></b>
    <b class="rnd_b3"></b>
    <b class="rnd_b2"></b>
    <b class="rnd_b1"></b>
  </b>
</div>

Powyższy kod można najprościej przetransportować do osobnego partiala i zapisac, np. w katalogu app/views/shared/_rounded_box.html.erb w lekko zmodyfikowanej wersji:

  <div class="rnd_container">
    <b class="rnd_top">
      <b class="rnd_b1"></b>
      <b class="rnd_b2"></b>
      <b class="rnd_b3"></b>
      <b class="rnd_b4"></b>
    </b>
  <div class="rnd_content"> 
    <h2><%= body %></h2>
  </div>
    <b class="rnd_bottom">
      <b class="rnd_b4"></b>
      <b class="rnd_b3"></b>
      <b class="rnd_b2"></b>
      <b class="rnd_b1"></b>
    </b>
  </div>

Sama metoda helpera wygląda następująco:

  def rounded_box(options={}, &block)
    options.merge!(:body => capture(&block))
    concat(render(:partial => "shared/rounded_box", :locals => options), block.binding)
  end

Jak widać oprócz bloku możemy przekazać do partiala swoje zmienne podając je jako parametry wywołania helpera. Cała magia polega na przechwyceniu zawartości bloku wywołaniem capture(&block) i skonkatenowaniem jej z naszym boksem.

Rozwiązanie szybkie, proste i jakże czytelne!

Tagi:

Manipulowanie czasem w testach w aplikacjach Ruby

05 lipca 2008, 00:43:31

Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 1 komentarz

Czasem, szczególnie podczas pisania testów, zachodzi potrzeba manipulowania czasem, np. podczas testowania metod, które mają zwracać posortowane względem czasu utworzenia obiekty ActiveRecord.

W ostatnim projekcie podczas pisania spec-ów modeli korzystałem z takiego tworu:

describe SomeClass do
  describe "by_date" do
    it "should return objects sorted by creation date" do
      Time.advancing_by_days(-1) do
        @earlier = SomeClass.new(:some => "values")
      end
      @normal = SomeClass.new
      Time.advancing_by_days(1) do
        @later = SomeClass.new(:some => "other value")
      end
      SomeClass.by_date.should == [@later, @normal, @earlier]
    end
  end
end

O ile mi wiadomo, Railsy nie mają standardowo wbudowanych klas do manipulowania czasem, czy metodą Time.now.

Stworzenie metod realizujących powyższą funkcjonalność jest bardzo proste. Wystarczy stworzyć plik, np. time_extensions.rb z poniższą zawartością:

require 'time'

if !Time.respond_to?('old_now')
  Time.class_eval {
    @@advance_by_days = 0
    @@advance_by_minutes = 0
    cattr_accessor :advance_by_days, :advance_by_minutes

    class << Time
      alias old_now now
      def now
        if Time.advance_by_days != 0
          return Time.at(old_now.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
        elsif Time.advance_by_minutes != 0
          return Time.at(old_now.to_i + Time.advance_by_minutes * 60)
        else
          old_now
        end
      end
      def advancing_by_days(days=0)
        Time.advance_by_days = days
        yield
        Time.advance_by_days = 0
      end
      def advancing_by_minutes(minutes=0)
        Time.advance_by_minutes = minutes
        yield
        Time.advance_by_minutes = 0
      end
    end
  }
end

Jak widać korzystamy z możliwości, jakie daje monkey pathing i modyfikujemy standardową metodę Time.now, uprzednio tworząc do niej alias. W powyższym kodzie oprócz zmiany czasu o zadaną liczbę dni, możemy zmieniać go o zadaną liczbę minut.

Aby korzystać z powyższych metod w spec-ach aplikacji railsowej, wystarczy wrzucić plik z powyższym kodem do katalogu spec aplikacji i dopisać do pliku spec_helper.rb linijkę:

require 'time_extensions'

Tagi:

Wcześniejsze wpisy Nowsze wpisy