Благотворительный, он же первый проект на рельсах

30 Июл
2012

Доброго времени суток.

Решил поделиться опытом изучения Ruby-on-rails с нуля, однако не стану затрагивать темы по написанию простых программ типа Hello World, покажу сразу на своем примере как освоил азы данного фреймворка на рабочем примере (статья ориентируется на начинающих разработчиков, дабы просто показать что где лежит).

Отмечу сразу, что до изучения Rails работал с ASP.NET.

Итак, решил сразу опробовать первый проект на чем-то рабочем. В итоге решил совместить приятное с полезным и переписать несложную админку сайта детского дома, который сделал года 2,5 назад в качестве благотворительного проекта, с NET на Rails.

Были 2 причины переписания админки, первую уже назвал, а вторая это постоянный непонятный апгрейд хостинга, после чего вылезали глюки на сайте или вовсе все слетало, а затачивать каждый раз сайт под хостинг довольно хлопотно, в итоге решил переписать все под более надежное и удобное решение.

Функционал сайта подразумевался простой — создание-редактирование-удаление текстовых страниц и новостей.
В общем, ничего сложного и особенного.

Средства разработки — RoR 3 + PostgreSQL + GIT

В данной статье для начала расскажу как создать простые текстовые страницы с ckeditor и сделать их сортировку

Начнем — создадим новый проект командой
rails new myapp1


Прежде всего отредактируем gemfile, добавив нужные gem-ы (писал зимой, версии могут быть не самыми свежими)

source ‘http://rubygems.org’

gem ‘rails’, ‘3.1.3’
gem ‘pg’, ‘0.12.2’ # БД
gem ‘therubyracer’, ‘0.9.9’ # для вызова javascript
gem ‘jquery-rails’, ‘1.0.19’

# Для upload и ckeditor
gem ‘paperclip’, ‘3.1.2’
gem ‘ckeditor’, ‘3.6.3’

#Постраничный вывод
gem ‘will_paginate’, ‘3.0.3’
gem ‘bootstrap-will_paginate’, ‘0.0.7’

group :assets do
gem ‘sass-rails’, ‘3.1.5’ # создает css.scss файл
gem ‘coffee-rails’, ‘3.1.1’ # создает js.coffee файл
gem ‘uglifier’, ‘>= 1.0.3’ # оболочка для UglifyJS JavaScript
end

group :development do
gem ‘rspec-rails’, ‘2.9.0’ # RSpec тесты
gem ‘annotate’, ‘2.4.1.beta1’ # Описание для модели
# gem ‘letter_opener’, ‘0.0.2’ # Ловим письма
end

group :test do
gem ‘rspec-rails’, ‘2.9.0’
gem ‘factory_girl_rails’, ‘1.7.0’ # Фабрика юзеров для тестирования
end

Комментарии для расшифровки что и где используется.

Для начала создадим модель для страниц (начнем именно с этого, а не с тестов, хотя вернее писать тесты, потом все остальное)

rails create model Page name:string title:string content:string metadescription:string metakeywords:string head:string ismenu:boolean


name — название страницы, будет отображаться в меню и на в качестве заголовка страницы
title — page title сверху
metadescription и metakeywords — мета
head — текстовый блок, в котором возможно размещать любые скрипты (располагается в head страницы)
ismenu — будет ли страница отображаться в меню
order_id — сортировка внутри страниц
parent_id — кто является парентом страницы

В итоге рельсы сгенерят модель и файл миграции

Миграция:

db\migrate\2012************_create_pages.rb

class CreatePages < ActiveRecord::Migration
  def change
    create_table :pages do |t|
      t.string :name, :default => "Empty string"
      t.string :title, :default => "Empty string"
      t.text :metadescription, :default => ""
      t.text :metakeywords, :default => ""
      t.text :head, :default => ""
      t.text :content, :default => "Empty string"
      t.boolean :ismenu, :default => false

      t.timestamps
    end
  end
end


Модель

app\models\page.rb

class Page < ActiveRecord::Base
  
  attr_accessible :name, :title, :content, :metadescription, :metakeywords, :head, :ismenu
  
end


здесь attr_accessible определяет атрибуты, которые участвую в массовом определении.

В миграции используется метод def change, можно использовать def up и def down, но change более удобно, т.к. он уже «знает» как откатить изменения.

timestamps — добавит в таблицу временные метки, столбцы, которые содержат информацию создания и изменения записи.
default — определяет значение по умолчанию.

Поле ID также создастся в таблице, его тут явно не указываем.

Запускаем миграцию
rake db:migrate

В итоге создается таблица.

Далее подготовим базу к тестам, командой
rake db:test:prepare

Итак, наша задача — реализовать создание-изменение-удаление страницы

Далее нужно написать тест создания новой страницы, но предварительно добавим в фабрику страницу для тестов
spec\factories.rb

Factory.define :page do |page|
  page.name "Name example page"
  page.title "Title example page"
  page.content "Content example page" 
  page.metadescription "metadescription example page"
  page.metakeywords "metakeywords example page"
  page.head "head example page"
  page.ismenu true
end


Создадим контроллер pages
rails generate controller Pages show edit update new create index destroy
Контроллер более подробно рассмотрим позже

Редактируем файл ресурсов config\routes.rb

добавляем
resources :pages


Далее пишем тест, который имитирует создание новой страницы, файл
spec\models\page_spec.rb

require 'spec_helper'

describe Page do
  
    before(:each) do
    @attr = {
      :name => "Example Name 1",
      :title => "Example 1",
      :content => "Content 1",
      :metadescription => "Meta Desc 1",
      :metakeywords => "Meta Key 1",
      :head => "Head 1"
      }
    end
      
  it "Create a new instance given valid attributes" do
    Page.create!(@attr)
  end
  

  it "Require Content" do
    no_content = Page.new(@attr.merge(:content => ""))
    no_content.should_not be_valid
 end
 
 it "Require Name" do
    no_name = Page.new(@attr.merge(:name => ""))
    no_name.should_not be_valid
 end
 
  
end


В файле предварительно создаются атрибуты, которые будут использоваться в тестах. Далее первый тест создает страницу с верными атрибутами, второй тест провалится, если контент страницы будет пустой, третий тест провалится, если название страницы будет пустым.

Все тесты приводит не буду, т.к. их можно понаписать сколь угодно много, покажу лишь несколько (например, можно протестировать на длину названия страницы, длину заголовка и т.п.).

Тест на длину страницы, раз вспомнил про него

it «Very long names» do
long_name = «a»*201
long_name_page = Page.new(@attr.merge(:name => long_name))
long_name_page.should_not be_valid
end

Итак, запускаем тесты, rspec=true autotest (использую автотест)

Получаем красные тесты, чтобы их исправить, добавляем в модель валидацию проверки на пустоту ( :presence => true) и длину (:length => {:maximum => 200}). В качестве максимальной длины установил 200 символов в title и name.

class Page < ActiveRecord::Base

    attr_accessible :name, :title, :content, :metadescription, :metakeywords, :head, :ismenu
  
   validates :name,
                :presence => true,
                :length => {:maximum => 200} 
 
   validates :title,
                :presence => true,
                :length => {:maximum => 200}
                
   validates :content,
                :presence => true
    
end


Автотест должен показать зеленый тест, все ок.

Редактируем контроллер pages_controller, добавляя

# Отображает конкретную страницу
def show    
    @page = Page.find(params[:id])    # находит страницу по айдишнику
  end

# редактирует конкретную страницу
  def edit
    @page = Page.find(params[:id])
  end
  
# обновляет конкретную страницу
  def update
    @page = Page.find(params[:id]) # находит страницу по айдишнику
    if @page.update_attributes(params[:page]) # Если было обновление страницы, то выводим сообщение, что все ок
      flash[:success] = t('activerecord.errors.controllers.message.attributes.page.page_update_success') # Здесь выводится сообщение из файла локализации, данный файл находится в config\locales\ru.yml
      redirect_to @page # перенаправление на страницу
    else
      render 'edit' # если ничего не было, рендерим форму редактирования страницы
    end  
  end
  
  def new
    @page = Page.new # создание новой страницы
  end
  
  def create
    @page = Page.new(params[:page])  
    if @page.save
      flash[:success] = t('activerecord.errors.controllers.message.attributes.page.page_create_success')
      redirect_to pages_path
    else
      render 'new'
    end
  end
  
  def index
    # здесь пока ничего не выводим, потом добавим отображение страниц в порядке сортировки 
  end  
 
  
  def destroy
    Page.find(params[:id]).destroy # удаление страницы
      
    
    flash[:success] = t('activerecord.errors.controllers.message.attributes.page.page_destroy_success')
    redirect_to pages_path
  end


Далее создаем общий layout сайта (app\views\layouts\application.html.erb) и вьюхи app\views\pages:

app\views\layouts\application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title><%= yield :title %></title>
  <%= javascript_include_tag "application" %>

  <%= stylesheet_link_tag    "application" %>
  
    <%= csrf_meta_tags %>

  <%= javascript_include_tag "/javascripts/ckeditor/ckeditor.js" %>

  
  <meta name="description" content="<%= yield :metadescription %>" />
  <meta name="keywords" content="<%= yield :metakeywords %>" />
  <%= yield :head %>
  
</head>
<body>

<div class="div-body-width">	
<%= render 'layouts/header' %>

	<% flash.each do |key, value| %>
          <div class="div-flash-<%= key %>"><%= value %></div>
	<% end %>

<%= yield %>


<%= debug(params) if Rails.env.development? %>
</div>
</body>
</html>


Header для layout
<header class="header">
	<nav>  	 
   	 <ul class="ul-header"> 
    	    <% for page in all_menu_pages %>
			<li class="li-header"><%= link_to page.name, page, :class => "a-menu" %></li>
		<%  end %>
   	 </ul>   	 
 	</nav> 	 
</header>


Хелпер для заголовка (app/helpers/pages_helper.rb)

module PagesHelper  
  def all_menu_pages
    return Page.find(:all, :conditions => "ismenu = TRUE")
  end 
end


Непосредственно вьюхи app\views\pages:

1) show.html.erb

<% content_for :title, @page.title %>
<% content_for :metadescription, @page.metadescription %>
<% content_for :metakeywords, @page.metakeywords %>
<% content_for :head do %>
  <%= raw @page.head %>
<% end %>

<div class="div-content">
	<h1><%= raw @page.name %></h1>
	<%= raw @page.content %>
</div>


Здесь raw используется, чтобы вставить данные «дослвно»

2) new.html.erb

<h1><%= t('page.page_new_welcome') %></h1>


<%= form_for(@page) do |f| %>
  <%= render 'fields', :f => f %>
  <div class="div-actions">
    <%= f.submit t('buttons.save') %>
  </div>
<% end %>


edit.html.erb

<h1><%= t('page.page_edit_welcome') %></h1>


<%= form_for(@page) do |f| %>
  <%= render 'fields', :f => f %>
  <div class="div-actions">
    <%= f.submit t('buttons.save') %>
  </div>
<% end %>


Форма (файл _fields.html.erb в той же папке)

 <%= render 'layouts/error_page_messages', :object => f.object %>

  <div class="div-field">
    <%= f.label :name, t('field.page_name') %><br />
    <%= f.text_field :name, :class => "text-field" %>
  </div>

  <div class="div-field">
    <%= f.label :title, t('field.page_title') %><br />
    <%= f.text_field :title, :class => "text-field" %>
  </div>
  
  <div class="div-field">
    <%= f.label :metakeywords, t('field.page_meta_keywords') %><br />
    <%= f.text_field :metakeywords, :class => "text-field" %>
  </div>
  
  <div class="div-field">
    <%= f.label :metadescription, t('field.page_meta_description') %><br />
    <%= f.text_field :metadescription, :class => "text-field" %>
  </div>
  
  <div class="div-field">
    <%= f.label :head, t('field.page_head') %><br />
    <%= f.text_area :head, :class => "textarea-head" %>
  </div>  

  <div class="div-field">
    <%= f.label :content, t('field.page_content') %><br />
    <%= f.cktext_area :content, :class => "textarea-content" %>
  </div>
  
  <div class="div-field">
    <%= f.check_box :ismenu, :class => "checkbox-field" %><%= f.label :ismenu, t('field.page_is_menu') %>    
  </div>


Рендерим ошибки ‘layouts/error_page_messages’

<% if object.errors.any? %>
  <div class="div-error-explanation">
    <h2><%= t('activerecord.errors.controllers.message.attributes.page.page_update_error')%> (<%= pluralize(object.errors.count, "error") %>)
        

<%= object.class.to_s.underscore.humanize.downcase %></h2>
        

    <p><%= t('activerecord.errors.controllers.message.attributes.auth.error_fields')%></p>
    

    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>


Форма рендерится во вьюхи создания или редактирования страницы.

В принципе, можно сделать тесты для создания-редактирования страницы (spec/controllers/pages_controller_spec.rb)
render_views

  describe "GET 'show'" do
    before(:each) do
      @page = Factory(:page)
    end
    
    it "should be successful" do
      get :show, :id => @page
      response.should be_success
    end
    
    it "all pages in menu should be successful" do
        get :show, :id => @page
        assert_select "header nav ul li", {:minimum=>1} # проверяем есть ли хотя бы один пункт меню в списке
    end       
       
   end


  describe "GET 'new'" do
    before(:each) do
      @page = Factory(:page)
# Здесь следует проверять еще юзера на админа, но в данной статье этого не рассматриваю
    end
    
    it "returns http success" do
      get 'new'
      response.should be_success
    end
  end

# Тесты на создание

 describe "POST 'create'" do
     
    before(:each) do
      @page = Factory(:page)
      # @user = Factory(:user) 
      # test_sign_in(@user) Проверка на админа, но здесь не используется
    end

    describe "failure" do

      before(:each) do
        @attr = { :name => "", :title => "", :content => ""} # провальные атрибуты
      end

      it "should not create a page" do
        lambda do
          post :create, :page => @attr
        end.should_not change(Page, :count)
      end    

    it "should render the 'new' page" do
        post :create, :page => @attr
        response.should render_template('new') # рендерится ли новая форма 
      end
 
    end
  end
  

  describe "GET 'edit'" do
    before(:each) do
      @page = Factory(:page)
    #  @user = Factory(:user)
   #   test_sign_in(@user)
    end

    it "should be successful" do
      get :edit, :id => @page
      response.should be_success
    end
  end



Тесты на обновление

describe "PUT 'update'" do

    before(:each) do
      @page = Factory(:page
    end

    describe "failure" do

      before(:each) do
        @attr = { :name => "", :title => "", :content => ""}
      end

      it "should render the 'edit' page" do
        put :update, :id => @page, :page => @attr
        response.should render_template('edit')
      end

    end

    describe "success" do

      before(:each) do
        @attr = { :name => "New Page Update", :title => "New Page Title Update", :content => "New Page Content Update" }
      end

      it "should change the page's attributes" do
        put :update, :id => @page, :page => @attr
        @page.reload
        @page.name.should == @attr[:name]
        @page.title.should == @attr[:title]
        @page.content.should == @attr[:content]
      end

      it "should redirect to the page show page" do
        put :update, :id => @page, :page => @attr
        response.should redirect_to(page_path(@page))
      end

      it "should have a flash message" do
        put :update, :id => @page, :page => @attr
        flash[:success].should == I18n.t('activerecord.errors.controllers.message.attributes.page.page_update_success') # отображается ли верное сообщение из файла локализации
      end
    end
  end
  


Тесты на удаление

 describe "DELETE 'destroy'" do

    before(:each) do
      @page = Factory(:page)
    end


    describe "as an admin user" do

      it "should destroy the page" do
       lambda do
          delete :destroy, :id => @page
        end.should change(Page, :count).by(-1)
      end

      it "should redirect to the pages page" do
        delete :destroy, :id => @page
        response.should redirect_to(pages_path)
      end
        
      
    end
  end


Тесты должны пройти.

В следующей статье рассмотрю как создавать сортировку страниц, путем drag-and-drop
По материалам Хабрахабр.



загрузка...

Комментарии:

Наверх