Leniwiec – klon Pastie

[Bradypus tridactylus]

To klon Pastie napisany w Rails 3 + ActiveRecord.

Czym jest „pastie”? Oto przykłady:

Jak stworzyć klona pastie w Sinatrze + Datamapper opisał Nick Plante, Clone Pastie with Sinatra & DataMapper 0.9.

Nasz „Leniwiec” ma umożliwiać wklejanie fragmentów kodu. Kod będziemy wklejać do formularza z przyciskiem „Wklej”. Po kliknięciu tego przycisku, wklejony kod zapisujemy w tabelce i przekierowujemy się na stronę z wklejonym kodem, na przykład:

http://localhost:3000/128

Od tej chwili, po wsze czasy (no, dopóty, dopóki nie padnie macierz dyskowa na Sigmie), wklejony kod będzie dostępny pod tym URL.

W następnej wersji dodamy do każdej strony formularz umożliwiający wyszukiwanie wklejonych fragmentów kodu zawierających podaną frazę.

A tak wygląda strona główna pastie.org:

[http://pastie.org]

Zaczynamy od wygenerowania rusztowanie aplikacji:

rails localhost

Oraz nauczenia Railsów odmiany słowa leniwiec. W pliku inflections.rb wpisujemy:

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular 'leniwiec', 'leniwce'
end

Projektujemy routing

[MVC]

Cytat: „The routing ActionController::Routing module provides URL rewriting in native Ruby. It’s a way to redirect incoming requests to controllers and actions. This replaces mod_rewrite rules. Best of all, Rails' Routing works with any web server. Routes are defined in config/routes.rb.” [więcej]

Strona główna:

http://localhost:3000/

Wklejone fragmenty kodu:

http://localhost:3000/:number

Wyszukiwanie:

http://localhost:3000/search?query=def

Wyniki:

http://localhost:3000/search

Routing:

# root_path, root_url
map.root :conditions => { :method => :get },
  :controller => 'leniwce',
  :action => 'index'

# leniwce_url(:number)
map.leniwce ':number',
  :conditions => { :method => :get },
  :controller => 'leniwce',
  :action => 'show',
  :number => /\d+/

# create_path
map.create '',
  :conditions => { :method => :post },
  :controller => 'leniwce',
  :action => 'create'

Uwaga: Strona główna aplikacji to strona z widokiem new. Na stronie głównej wklejamy kod do zapamiętania. Dlatego powinnienem to zrobić tak:

# root_path
map.root :controller => "leniwce", :action => "new"

Dalej bez zmian.

# search_path
map.search 'search',
  :conditions => { :method => :get },
  :controller => 'leniwce',
  :action => 'search'

Generujemy model i migrujemy:

[leniwiec.rb]
script/generate model Leniwiec lang:string body:text
rake db:migrate

Generujemy kontroller:

script/generate controller Leniwce index show search

Dopiero teraz możemy obejrzeć routing:

rake routes

I próbnie uruchomić aplikację.

Co działa? A co nie działa? Wpisujemy kilka URL. Przyglądamy się temu co jest wypisywane na konsoli.

Jazda obowiązkowa: ćwiczenia na konsoli

[MVC]

Przykłady do wpisania na konsoli SQlite, script/dbconsole:

insert into leniwce (lang, body)
    values("ruby", "puts 1");
insert into leniwce (lang, body)
    values("html", "<html></html>");

I – na konsoli Rails, script/console:

Leniwiec.all
Leniwiec.all(:conditions => ["lang = ?", "ruby"])
Leniwiec.all(:conditions =>
  ["lang = ? and body like ?", "ruby", "%puts%"])

Więcej przykładowych poleceń jest do wklejenia stąd

w = Leniwiec.new :lang => "css",
    :body => "h1 { color: red; }"
w.save

Leniwiec.create :lang => "css",
    :body => "h1 { background-color: blue; }"

Widoki

[MVC]

Zaczynamy od widoku index.html.erb z formularzem do wklejania kodu:

<% form_for :leniwiec, :url => {
      :controller => "leniwce",
      :action => "create" } do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :lang, "Wybierz język" %><br />
    <%= f.select :lang, @languages %>
  </p>
  <p>
    <%= f.label :body, "Wklej kod:" %><br />
    <%= f.text_area :body, :cols => 80, :rows => 20 %>
  </p>
  <p>
    <%= f.submit 'Wklej' %>
  </p>
<% end %>

Od razu też wrzucimy do katalogu layouts layout application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Leniwce</title>
  <%= stylesheet_link_tag 'application', 'uv' %>
</head>
<body>
  <h1>Leniwce</h1>

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

  <%= yield %>

</body>
</html>

Plik uv.css będzie używany przy kolorowaniu składni. Skopiowałem go z katalogu stylesheets tego samouczka.

Wejście na stronę http://localhost:3000/ daje błąd:

NoMethodError in Leniwce#index

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
…
<%= f.select :lang, @languages %>

Zmienna @languages nie istnieje. Aby błąd się nie pojawiał musimy ją zdefiniować.

[leniwce_controller.rb]

Dopisujemy w pliku leniwce_controller.rb w metodzie index:

class LeniwceController < ApplicationController
  def index
    @languages = ["ruby", "html", "css"]
  end

Jeszcze raz wchodzimy na stronę http://localhost:3000/, teraz już działa, wpisujemy kawałek kodu, klikamy przycisk „Wklej” i dostajemy stronę z:

Unknown action
No action responded to create. Actions: index, search, and show

No to definiujemy akcję create. Ponownie zaglądamy do pliku leniwce_controller.rb, gdzie definiujemy metodę create:

def create
  @leniwiec = Leniwiec.new(params[:leniwiec])
  if @leniwiec.save
    flash[:notice] = 'Umieszczono leniwca w bazie.'
    redirect_to leniwce_path(@leniwiec.id)
  else
    render :action => "index"
  end
end

Teraz wyświetla się wygenerowana stronka show.html.erb na której ma się pojawić przed chwilą wklejony i zapisany w bazie kod. Zmieniamy ją na:

<h3>Leniwiec: <%= @leniwiec.lang %></h3>
<%= @leniwiec.body %>
<%= link_to 'Nowy leniwiec', root_path %>

Teraz po przekierowaniu dostajemy błąd:

NoMethodError in Leniwce#show
Showing app/views/leniwce/show.html.erb where line #1 raised:
You have a nil object when you didn't expect it!

Oznacza to, że w metodzie show musimy wyciągnąć leniwca z bazy. Robimy to tak:

def show
  @leniwiec = Leniwiec.find(params[:number])
end

Dodajemy kolorowanie składni

Zmieniamy kod metody show:

class LeniwceController < ApplicationController
  def show
    @leniwiec = Leniwiec.find(params[:number])
    @leniwiec.body = ::Uv.parse(@leniwiec.body, 'xhtml', @leniwiec.lang, false, "dawn")
  end

oraz dopisujemy w pliku environment.rb:

require 'uv'
Rails::Initializer.run do |config|
  config.gem 'ultraviolet'

Następna wersja „Leniwca”

Zaimplementujemy wyszukiwanie fragmentów kodu zawierających podaną frazę. Jak takie rzeczy się robi można podejrzeć na screencaście Simple Search Form.

W pliku application.html.erb dodamy formularz. Tym razem użyjemy form_tag a nie form_for, dlaczego?

<% form_tag search_path, :method => 'get' do %>
  <p>
    <%= text_field_tag :search, params[:search] %>
    <%= submit_tag "Wyszukaj", :name => nil %>
  </p>
<% end %>

Jaką funkcję spełnia :name => nil? Technicznie żadną. To jaki sens ma ten kawałek kodu?

W pliku leniwce_controller.rb piszemy metodę search:

def search
  @search = params[:search]
  @leniwce = Leniwiec.search(@search)

  # kolorowanie składni
  @leniwce.each do |leniwiec|
    leniwiec.body = ::Uv.parse(leniwiec.body, 'xhtml', leniwiec.lang, false, "dawn")
  end
end

Implementujemy Leniwiec.search:

def self.search(search)
  if search
    all(:conditions => ['body LIKE ?', "%#{search}%"])
  else
    all
  end
end

I tworzymy widok na wyszukane fragmenty kodu:

<h3>Wyniki wyszukiwania: <%= @search %></h3>
<% @leniwce.each do |leniwiec| %>
  <p><b><%= leniwiec.lang %></b></p>
  <%= leniwiec.body %>
<% end %>

Filter parameter logging

Pamiętamy też o skróceniu logów (dlaczego?):

class ApplicationController < ActionController::Base
  # Scrub sensitive parameters from your log
  filter_parameter_logging :body
end

Final touches

Testowanie: Michelangelo Altamore, Create a simple code snippet app with Rails. Rails Magazine, Issue 3. 2009.

Pozostaje jeszcze osuszyć kod (jaki?) oraz odrobić zadania.

TODO

Full text searching with PostgreSQL