Awesome Feature Suggest 1
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.
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.