Awesome Feature Suggest 1

by: Dirk Siemers | posted: February 15th, 2011

Ich habe mich am Sonntag mit einem Artikel von der Seite Tutorialzine befasstet. Das Tutorial beschreibt den Aufbau einer simplen Feature Request App mit PHP, MySQL und jQuery.

HIER geht´s direkt zur app

Da ich noch nicht direkt ein Projekt mit Rails 3 entwickeln konnte, habe ich mich entschlossen den Ansatz von Tutorialzine aufzunehmen und eine kleine Rails 3 app zu schreiben, nur BESSER!

Du brauchst für mein Tutorial RVM!

    rails new suggest
    echo "rvm ruby-1.9.2@suggest --create" > suggest/.rvmrc
    cd suggest

    ================================================================
    = Trusting an .rvmrc file means that whenever you cd into the  =
    = directory RVM will excecute this .rvmrc script in your shell =
    =                                                              =
    = Now that you have examined the contents of the file, do you  =
    = wish to trust this .rvmrc from now on?                       =
    ================================================================

    (yes or no) > yes

Wir werden aufgefordert die angelegte .rvmrc zu vertrauen, was natürlich der Fall ist, denn wir wollen ein seperates gemset für unsere neue app anlegen.

Für unser SCM nutzen wir natürlich git. Ich persönlich verfolge den Ansatz von git-flow und initialisiere somit meinen branches mit:

    git flow init

    No branches exist yet. Base branches must be created now.
    Branch name for production releases: [master] 
    Branch name for "next release" development: [develop] 

    How to name your supporting branch prefixes?
    Feature branches? [feature/] 
    Release branches? [release/] 
    Hotfix branches? [hotfix/] 
    Support branches? [support/] 
    Version tag prefix? []

Sofern nicht anders benötigt, einfach ENTER drücken ;) Jetzt checken in welchen branch wir uns befinden:

    git branch

    * develop
      master

Wir sehen, dass wir uns jetzt im develop branch befinden.

    git add .
    git commit -a -m "initial commit"
    bundle

Zunächst den initial commit ausführen und als nächstes bundeler laufen lassen, damit wir in unserem frischen gemset auch alle benötigten gems haben.

Okay, das Projekt ist soweit um Modelle anzulegen. Fangen wir mit den suggestions an.

    rails generate model suggestion suggestion:string votes_up:integer votes_down:integer rating:integer
    rake db:migrate
    git add .
    git commit -a -m "adding suggestion model"

Füllen wir das model app/models/suggestion.rb mit Leben:

class Suggestion < ActiveRecord::Base
  before_create :set_rating
  
  validates :suggestion, :presence => true
  validates :suggestion, :uniqueness => true
  
  def vote_up
    self.update_attributes({
     :votes_up => self.votes_up.to_i+1,
     :rating => self.rating.to_i+1
    })
  end
  
  def vote_down
    self.update_attributes({
     :votes_down => self.votes_down.to_i+1,
     :rating => self.rating.to_i-1
    })
  end
  
  private
    def set_rating
      self.rating, self.votes_up, self.votes_down = 0, 0, 0
    end
end

Die beiden Methoden vote_up und vote_down erleichtern später die Auswertung. Mit der Methode set_rating initialisiere ich explizit vor dem Erstellen des Models die Wertungsattribute. Zusätzlich spendiere ich dem model die Validierung auf Vorhandensein und Einmaligkeit des Attributs suggestion.

Das sind schon viele Kleinigkeiten, die wir dem Model auftragen zu beherzigen, aber passt das auch? Um z.B. die Validierungen zu testen, könnten wir folgenden Unit Test schreiben:

require 'test_helper'

class SuggestionTest < ActiveSupport::TestCase
  fixtures :suggestions
  
  test "suggestion attributes must not be empty" do
    suggestion = Suggestion.new
    assert suggestion.invalid?
    assert suggestion.errors[:suggestion].any?
  end
  
  test "suggestion is not valid without a unique suggestion" do
    suggestion = Suggestion.new(:suggestion  => suggestions(:rails).suggestion)
    assert !suggestion.save
    assert_equal "has already been taken", suggestion.errors[:suggestion].join('; ')
  end
  
end
rails:
  suggestion: Create a Ruby on Rails Tutorial
  votes_up: 0
  votes_down: 0
  rating: 0

blog:
  suggestion: Add new blog entry
  votes_up: 0
  votes_down: 0
  rating: 0

Und was sagt unser Unit Test dazu?

    rake test:units

    Started
    ..
    Finished in 0.194499 seconds.

    2 tests, 4 assertions, 0 failures, 0 errors, 0 skips

Sauber, 2 tests, 4 assertions und keine Fehler. So kann das weitergehen. Also, Quellcode commiten und einen Controller für unser Model mit einer index action anlegen. Außerdem arbeite ich gerne mit haml und jquery, daher packe ich sie in das gemfile und starte bundler. Danach hole ich mir jQuery in das Projekt (rails.js muss überschrieben werden) und “verschiebe” das application .erb layout nach .haml.

    git commit -a -m "validating unique not empty suggestion"
    rails g controller suggestions index
    echo "gem 'haml'" >> Gemfile
    echo "gem 'jquery-rails', '>= 0.2.6'" >> Gemfile
    bundle
    rails generate jquery:install --ui
    mv app/views/layouts/application.html.erb app/views/layouts/application.html.haml

Hier nun der Inhalt für das application haml layout:

!!! 5
%html
    %head
        %meta{'http-equiv' => 'Content-Type', :content => 'text/html; charset=utf-8'}/
        %title Suggest
        = stylesheet_link_tag :all
        = javascript_include_tag :defaults
        = csrf_meta_tag
    
    %body
        #page
            #heading.rounded
                %h1
                    Feature Suggest
                    %i for Tutorialzine.com
                
            = yield

Der Inhalt vom Controller app/controllers/suggestions_controller.rb :

class SuggestionsController < ApplicationController
  before_filter :load_suggestions
  
  def index
    @suggestion = Suggestion.new
  end
  
  def create
    @suggestion = Suggestion.new(params[:suggestion])

    respond_to do |wants|
      if @suggestion.save
        wants.html { redirect_to root_path }
      else
        wants.html { render :action => "index" }
      end
    end
  end
  
  private
  def load_suggestions
    @suggestions = Suggestion.order("rating DESC")
  end
  
end
    rm public/index.html
    mv app/views/suggestions/index.html.erb app/views/suggestions/index.html.haml
    touch public/stylesheets/styles.css

Das CSS und die Bilder habe ich mir bei dem Artikel von tutorialzine.com ausgeliehen. Das Css kommt in die public/stylesheets/styles.css, die Bilder in den Ordner public/images/

Der Inhalt der routes.rb :

Suggest::Application.routes.draw do
  resources :suggestions
  root :to => "suggestions#index"
end

Der Inhalt vom index View app/views/suggestions/index.haml :

%ul.suggestions
    - @suggestions.each do |suggestion|
        = render :partial => 'suggestion', :locals => {:suggestion => suggestion}
        
= form_for @suggestion, :as => :suggestion, :url => suggestions_path, :html => { :id => "suggest" } do |f|
    %p
        = f.text_field :suggestion, :id => "suggestionText", :class => "rounded"
        = f.submit "Submit", :disable_with => 'Submiting...', :id => "submitSuggestion"

Die Aktivität/Inaktivität der suggestion definiere ich vorerst aus Der Inhalt vom Partial suggestion app/views/suggestions/_suggestion.haml :

%li{:id => "s_#{suggestion.id}"}
    %div{:class => "vote #{inactive ? 'inactive' : 'active'}"}
        %span.up
        %span.down
    .text= suggestion.suggestion
    .rating= suggestion.rating

Als nächstes können wir funktionalen Test erstellen um den Controller zu überprüfen:

require 'test_helper'

class SuggestionsControllerTest < ActionController::TestCase
  test "should get index" do
    get :index
    assert_response :success
    assert_select "#page ul.suggestions li", 2
    assert_tag "form", :attributes => {:action => "/suggestions", :method => "post" }
  end
  
  test "should create suggestion" do 
    assert_difference('Suggestion.count', 1) do
      post :create, :suggestion => {:suggestion => "Create functional tests"}
    end
    assert_redirected_to root_path
  end
  
  test "should not create suggestion" do
    assert_difference('Suggestion.count', 0) do
      post :create, :suggestion => {:suggestion => "Create a Ruby on Rails Tutorial"}
    end
    assert_template :index
  end
  
end
    rake test:functionals

    Started
    ...
    Finished in 0.316923 seconds.

    3 tests, 7 assertions, 0 failures, 0 errors, 0 skips

Okay, wir können sicher sein, dass unser Controller macht, was er tun soll.

Bevor ich hier die 300 LOC Grenze für einen Blogeintrag überschreite, werde ich den suggestion_votes und der meiner awsome Variante des Feature Requests jeweils einen eigenen Blogeintrag widmen.

Fork me on GitHub