toe waarin de Views zullen worden gestopt. Om het rails.js bestand te genereren kun je de jquery alias aanroepen die ik een paar hoofdstukken terug heb laten zien. Mocht je deze zijn vergeten kun je het volgende in de terminal uitvoeren:
curl -L http://github.com/rails/jquery-ujs/raw/master/src/rails.js > public/javascripts/rails.js
Het volgende wat we doen is een simpel CSS bestand toevoegen. Stop ‘style.css’ in de stylesheets map in /public en voeg het volgende hieraan toe:
body { margin: 0; padding: 0; font-family: Arial, sans-serif; background: #ddd; text-align: center; } #wrapper { width: 800px; margin: 0 auto; padding: 25px; background: #fff; text-align: left; } h1 { margin-top: 0; font-size: 24px; font-weight: normal; } p { margin-top: 0; }
In Rails 3.1 werkt dit, zoals ik al eerder heb aangegeven, een stuk anders. Er is een assets map in /app toegevoegd waarin alle stylesheets en javascripts komen te staan. Ook zit hier standaard een application.css en application.js bestand waarnaar ook automatisch wordt verwezen in de layout. De jQuery bestanden worden ook automatisch ingeladen, waardoor dit ook niet nodig is. Het enige wat je moet doen van het bovenstaande is de CSS toevoegen aan application.css.
Nu kunnen we doorgaan met de CRUD. Laten we eerst de show actie aanmaken in de Controller.
We moeten in de index View nog even links maken van alle to-do’s. 182 - Instappen in Ruby on Rails 3 - Robin Brouwer
... <%= div_for(todo) do %><%= link_to(todo.title, todo) %><% end %> ...
Nu kun je van de index actie naar de show actie gaan door op de to-do’s te klikken. De volgende stap is een formulier maken om nieuwe to-do’s te maken. Dit is de new actie.
Kijk eens aan. We gebruiken dezelfde link om terug te gaan als bij de show View. Laten we hier een helper van maken. Open /app/helpers/application_helper.rb en stop het volgende erin:
module ApplicationHelper def back_to(path) link_to("« Back".html_safe, path) end end
Laten we nu een create actie aanmaken in de Controller. Dit is de actie waar het formulier naartoe wordt gestuurd.
def create @todo = Todo.new(params[:todo]) if @todo.save redirect_to(@todo, notice: "To-do has been created.") else render("new") end end
Als het opslaan is gelukt wordt je naar de show actie verwezen en anders wordt de new View opnieuw getoond. Er wordt om de label en het veld een
met als class ‘field_with_errors’ toegevoegd als er iets fout is. Laten we hier CSS voor maken:
.field_with_errors { display: inline; color: red; }
De laatste stap is om een link naar de new actie te maken. Deze link hebben we alleen op de index pagina nodig. Stop dit aan het einde van index.html.erb:
<%= link_to("Create new To-do", [:new, :todo]) %>
Laten we hier gelijk wat CSS voor maken:
.new_todo { margin-top: 24px; } .new_todo a { padding: 12px; background: #777; color: #fff; text-decoration: none; }
Als je nu op deze link klikt kun je een nieuwe to-do aanmaken en opslaan. Het volgende is een to-do kunnen aanpassen en verwijderen. Dit gaan we vanuit de show View doen. Stop het volgende aan het einde van deze View:
184 - Instappen in Ruby on Rails 3 - Robin Brouwer
<%= link_to("Edit To-do", [:edit, @todo]) %> | <%= link_to("Delete To-do", @todo, method: :delete, confirm: "Are you sure?") %>
De destroy actie maken is het makkelijkste, dus laten we dit eerst doen. Stop het volgende in de Controller:
def destroy @todo = Todo.find(params[:id]) @todo.destroy redirect_to(:todos, notice: "To-do has been deleted.") end
Als je nu op de destroy link klikt zal de to-do worden verwijderd. Eerst krijg je een JavaScript pop-up te zien die vraagt of je het zeker weet. Hier in het volgende hoofdstuk meer over. Laten we nu de edit en update acties in de Controller stoppen. De edit actie is om het formulier te laten zien en update is waar het formulier naartoe wordt gestuurd.
def edit @todo = Todo.find(params[:id]) end def update @todo = Todo.find(params[:id]) if @todo.update_attributes(params[:todo]) redirect_to(@todo, notice: "To-do has been updated.") else render("edit") end end
De volgende stap is om de View voor de edit actie te maken.
185 - Instappen in Ruby on Rails 3 - Robin Brouwer
<%= back_to(@todo) %>
<%= form_for(@todo) do |f| %>
<%= f.label(:title, "To-do:") %> <%= f.text_field(:title) %> <%= f.submit %>
<% end %>
Het formulier is precies hetzelfde als het formulier bij de new actie. Dit kunnen we daarom in een partial stoppen. Dit formulier noemen we ‘_form.html.erb’. We stoppen het formulier erin.
<%= form_for(@todo) do |f| %>
<%= f.label(:title, "To-do:") %> <%= f.text_field(:title) %> <%= f.submit %>
<% end %>
En roepen deze aan vanuit de new en edit acties:
<%= render("form") %>
En nu kunnen we de to-do’s bekijken, toevoegen, aanpassen en verwijderen. Het enige wat we nu nog moeten doen is de flash[:notice] laten zien in de application layout.
<% if flash[:notice].present? %>
<%= flash[:notice] %>
<% end %>
<%= yield %>
Laten we hier gelijk wat CSS voor maken:
186 - Instappen in Ruby on Rails 3 - Robin Brouwer
#flash { position: absolute; width: 100%; background: #000; color: #fff; padding: 10px 0; font-size: 16px; }
En om het helemaal gelikt te maken kunnen we wat JavaScript toevoegen in het application.js bestand:
$(function(){ if ($("#flash").length) { $("#flash").hide(); $("#flash").fadeIn(1000, function(){ setTimeout(fadeOutFlash, 3000); }); } function fadeOutFlash() { $("#flash").fadeOut(1000); } });
Nu wordt het bericht met een fade-in getoond en gaat deze na 3 seconden weg met een fade-out. We hebben nu een simpele CRUD gemaakt voor de to-do’s in onze applicatie. Er is echter nog genoeg te doen, dus laten we doorgaan naar het volgende gedeelte.
9.4 To-do’s afvinken Heel leuk allemaal dat we nu to-do’s kunnen beheren, maar de bedoeling is natuurlijk dat we to-do’s kunnen afvinken. Hiervoor zullen we een extra actie genaamd ‘complete’ aanmaken in de Controller. Als deze wordt aangeroepen zal de to-do worden afgevinkt. Hiervoor moeten we een member toevoegen aan de RESTful routing. We gaan immers één to-do aanpassen en het is niet een standaard CRUD actie. Verander daarom het volgende in routes.rb:
187 - Instappen in Ruby on Rails 3 - Robin Brouwer
resources :todos do member do put "complete" end end
Aangezien we een to-do aanpassen gebruiken we de HTTP method PUT. Nu kunnen we in de Controller deze actie aanmaken:
def complete @todo = Todo.find(params[:id]) @todo.update_attribute(:completed, true) redirect_to(@todo, notice: "To-do has been completed.") end
Nu kunnen we een link aanmaken die de to-do afvinkt. Deze stoppen we voor nu in de show actie.
<%= link_to("Complete To-do", [:complete, @todo], method: :put) %> | <%= link_to("Edit To-do", [:edit, @todo]) %> | <%= link_to("Delete To-do", @todo, method: :delete, confirm: "Are you sure?") %>
Als je nu op die link klikt zal de to-do worden afgevinkt. Als je dit wilt terugdraaien moet je een nieuwe actie aanmaken. Laten we deze ‘incomplete’ noemen.
resources :todos do member do put "complete" put "incomplete" end end
En dan ook een actie in de Controller.
188 - Instappen in Ruby on Rails 3 - Robin Brouwer
def incomplete @todo = Todo.find(params[:id]) @todo.update_attribute(:completed, false) redirect_to(@todo, notice: "To-do has been reassigned.") end
We moeten nu ook de link aanpassen. Hier een simpel voorbeeld met behulp van een ifstatement.
<% if @todo.completed %> <%= link_to("Reassign To-do", [:incomplete, @todo], method: :put) %> | <% else %> <%= link_to("Complete To-do", [:complete, @todo], method: :put) %> | <% end %>
En nu kunnen we taken afvinken en opnieuw activeren. Het volgende wat we kunnen doen is bij de index actie sorteren op afgevinkte taken en de afgevinkte taken anders laten zien. Eerst gaan we een scope maken in de Todo Model.
scope :order_by_completed, order("completed ASC")
Deze scope kunnen we dan aanspreken in de index actie om de to-do’s te sorteren:
def index @todos = Todo.order_by_completed end
In de index View kunnen we nu een extra class meegeven als de to-do is afgevinkt.
<% @todos.each do |todo| %> <%= div_for(todo, class: todo.completed ? "completed" : nil) do %> <%= link_to(todo.title, todo) %> <% end %> <% end %>
Er wordt een extra class aan toegevoegd als de to-do is afgevinkt. Dan kunnen we met CSS de to-do er anders uit laten zien:
189 - Instappen in Ruby on Rails 3 - Robin Brouwer
.completed { opacity: 0.5; }
Nu zie je de to-do’s die zijn afgevinkt iets lichter dan de rest, zodat je duidelijk ziet wat wel en niet is afgevinkt. De manier die ik hier heb laten zien is overigens niet de beste manier waarop je het afvinken van to-do’s voor elkaar kunt krijgen. Je kunt ook alles via de update actie laten gaan. Je hoeft dan niet extra acties aan te maken. In het volgende hoofdstuk zal ik laten zien hoe dit werkt in combinatie met JavaScript. Ik wilde je hier laten zien hoe je om moet gaan met acties die afwijken van de standaard CRUD acties.
9.5 Has many comments Een simpele to-do is vaak niet genoeg om te vertellen wat er moet gebeuren. Ook wil je zo nu en dan een status update plaatsen op een to-do, zodat je later precies ziet wat er is gedaan en wat er nog moet gebeuren. Dit kan je op verschillende manieren voor elkaar krijgen. Wij gaan dit doen door comments toe te voegen aan een to-do. Een to-do ‘has_many’ comments. Eerst moeten we een nieuwe Model aanmaken voor de comments die worden geplaatst. Voer het volgende uit in de terminal:
rails g model comment body:text todo_id:integer
Het migration bestand is al in orde doordat we de kolommen hebben meegegeven, dus kunnen we gelijk een migratie uitvoeren:
rake db:migrate
Het volgende wat we moeten doen is de relatie aanleggen tussen de twee Models:
190 - Instappen in Ruby on Rails 3 - Robin Brouwer
class Comment < ActiveRecord::Base belongs_to :todo end class Todo < ActiveRecord::Base has_many :comments end
Nu dit is gedaan kunnen we de CommentsController toevoegen die ervoor moet zorgen dat comments kunnen worden geplaatst.
rails g controller comments
Aangezien een comment altijd bij een to-do hoort zullen we gebruikmaken van nested resources. Verander het volgende bij de routes:
resources :todos do resources :comments ... end
Bij de show View van de TodosController willen we de comments laten zien. Laten we dit met een partial doen. Maak in /app/views/comments een partial genaamd ‘_comment.html.erb’ en stop het volgende erin:
<strong>Posted on <%= comment.created_at.strftime("%d/%m/%Y") %>:
<%= comment.body %>
Nu kunnen we aan het einde in de show View het volgende stoppen:
<%= render(@todo.comments) %>
Alle comments die bij de to-do horen komen daar nu te staan. De volgende stap is om een formulier te maken waarmee een comment kan worden toegevoegd. Dit zullen we in de
191 - Instappen in Ruby on Rails 3 - Robin Brouwer
show View van de TodosController doen. Eerst zullen we iets extra’s aan de Controller moeten toevoegen:
def show @todo = Todo.find(params[:id]) @comment = Comment.new end
We kunnen dit nu gebruiken om in de show View een formulier te maken.
<%= form_for([@todo, @comment]) do |f| %>
<%= f.label(:body, "Add a new comment:") %>
<%= f.text_area(:body) %>
<%= f.submit %>
<% end %>
Deze zal met de HTTP method POST verwijzen naar /todos/:todo_id/comments. De :todo_id kunnen we aanspreken in de CommentsController, waardoor we de comment goed kunnen koppelen.
class CommentsController < ApplicationController def create @comment = Comment.new(params[:comment]) current_todo.comments << @comment redirect_to(current_todo, notice: "Comment has been created.") end private def current_todo @current_todo ||= Todo.find(params[:todo_id]) end end
We hebben een speciale current_todo method gemaakt om de huidige to-do op te halen. We voegen de nieuwe comment dan toe aan deze to-do en verwijzen terug naar de show actie van de TodosController. Nu kun je comments toevoegen aan to-do’s.
192 - Instappen in Ruby on Rails 3 - Robin Brouwer
Je kunt nu de CRUD afmaken, zodat je comments kunt verwijderen en aanpassen. Denk om de nested resources! Je moet namelijk bij elke url de to-do meegeven om alles goed te laten werken.
9.6 Gebruikersbeheer Het volgende wat we in onze applicatie stoppen is gebruikersbeheer. We zullen ervoor zorgen dat een admin gebruikers kan aanmaken en dat gebruikers dan kunnen inloggen voor hun eigen to-do lijstjes. De gebruikers krijgen dan alleen de to-do’s te zien die van hun zijn. In een redelijk nieuwe railscast op http://railscasts.com wordt er speciaal aandacht besteed aan gebruikersbeheer. Hierin wordt uitgelegd hoe je een inlog systeem kunt maken zonder gebruik te maken van een plugin of gem. Authentication from scratch http://railscasts.com/episodes/250-authentication-from-scratch (TinyURL: http://tinyurl.com/5rbttya)
In Rails 3.1 is dit nog gemakkelijker geworden. Toevallig is hier ook een railscast over gemaakt: http://railscasts.com/episodes/270-authentication-in-rails-3-1.
Wij zullen dit net iets anders aanpakken. Ik raad je aan om na dit hoofdstuk een kijkje te nemen naar deze railscast om te zien hoe Ryan Bates, die alle railscasts maakt, het doet.
9.6.1 Gebruikers CRUD Het eerste wat we doen is een CRUD maken voor het beheren van gebruikers. Ik zal hiervoor een begin maken en ga er vanuit dat je zelf de CRUD kunt afmaken. Eerst moeten we een User Model hebben. rails g model user email:string
Open nu het migration bestand.
193 - Instappen in Ruby on Rails 3 - Robin Brouwer
class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :email t.timestamps end end def self.down drop_table :users end end
We focussen ons op de up method. We zullen naast een e-mailadres ook het wachtwoord van de gebruiker opslaan en een boolean toevoegen om te kijken of diegene een admin is. Een wachtwoord kun je natuurlijk niet in een tabel opslaan. Je moet deze eerst veranderen in een hash. Hiernaast gebruiken we een ‘salt’ om de hash nog beter te beveiligen.
create_table :users do |t| t.string :email t.string :password_hash t.string :password_salt t.boolean :admin t.timestamps end
Een hash is niet zoals de Hash in Ruby. Dit is de encryptie die je kunt uitvoeren op een String, zoals een wachtwoord. Een salt is een willekeurige String. Als je deze samenvoegt met het wachtwoord en van dit geheel een hash maakt, krijg je een behoorlijk veilig opgeslagen wachtwoord.
In Rails 3.1 is er een makkelijkere manier om wachtwoorden op te slaan. Je hoeft niet de salt in een aparte kolom op te slaan. Noem het veld ‘password_digest’ en maakt er een String van. Rails zal d.m.v. BCrypt het wachtwoord op een nog veiligere manier opslaan in alleen deze kolom. Je moet dan wel ‘bcrypt-ruby’ in de Gemfile stoppen.
Deze migration kunnen we nu uitvoeren.
194 - Instappen in Ruby on Rails 3 - Robin Brouwer
rake db:migrate
Het volgende wat we moeten doen is de Controller aanmaken voor de CRUD. We doen dit niet in een gewone Controller, maar in een namespace Controller. We gaan namelijk een admin namespace maken. Laten we eerst de routes hiervoor goed zetten.
namespace :admin do root to: "users#index" resources :users end
Nu dit is geregeld kunnen we de Controller aanmaken.
rails g controller admin/users
Nu dit is gedaan kunnen we een BaseController toevoegen aan de admin namespace.
rails g controller admin/base
Nu we een BaseController hebben kunnen we de UsersController hiervan laten erven.
class Admin::UsersController < Admin::BaseController end
Nu kunnen we een CRUD maken voor het beheren van de gebruikers. Voordat we dit doen moeten we echter nog wat dingen doen in de User Model. We kunnen immers de admin niet de password_hash laten invullen in een formulier. De admin moet een wachtwoord invullen en Rails moet hier automatisch een hash voor maken. Hiernaast willen we gelijk wat validatie eraan toevoegen.
195 - Instappen in Ruby on Rails 3 - Robin Brouwer
class User < ActiveRecord::Base attr_accessor :password before_save :encrypt_password validates_confirmation_of :password validates_presence_of :password, on: :create validates :email, presence: true, uniqueness: true def encrypt_password if password.present? self.password_salt = User.generate_salt self.password_hash = User.hash_secret(password, password_salt) end end private def self.generate_salt ActiveSupport::SecureRandom.hex(16) end def self.hash_secret(pass, salt) Digest::SHA1.hexdigest(pass + salt) end end
In Rails 3.1 hoef je alleen ‘has_secure_password’ aan te roepen in de Model om hetgeen wat hierboven staat voor elkaar te krijgen. Je moet dan wel een andere kolom hebben voor het wachtwoord: password_digest. Deze zal dan automatisch worden gevuld. Wat je hierboven ziet is dus niet meer nodig in Rails 3.1.
Ik zal hier van boven naar beneden doorheen lopen. Als eerste hebben we een attr_accessor die ervoor zorgt dat er een virtueel attribuut genaamd ‘password’ wordt aangemaakt. Deze kunnen we gebruiken in formulieren, zodat de gebruiker een wachtwoord kan invullen. Hierna komt een before_save die een method uitvoert. Deze method heet ‘encrypt_password’ en zorgt ervoor dat de password_salt en password_hash wordt gezet. Hier zometeen meer over. Hierna komt de validatie. Als eerste maken we een virtuele attribuut aan genaamd ‘password_confirmation’ door ‘validates_confirmation_of’ te gebruiken. Hiernaast checken we altijd bij de create actie of er een wachtwoord is ingevuld en zorgen we ervoor dat een e-mailadres altijd moet worden ingevuld en uniek moet zijn.
196 - Instappen in Ruby on Rails 3 - Robin Brouwer
De password_salt wordt ingevuld met een class method die ik heb toegevoegd genaamd ‘generate_salt’. Deze maakt een random String van 16 tekens. Er zijn verschillende manieren om een random String te genereren. Ik gebruik een method die in Rails zelf zit. Meer hierover op Stack Overflow. How best to generate a random string in Ruby http://stackoverflow.com/questions/88311 (TinyURL: http://tinyurl.com/yzc5bw9)
Toevallig is ActiveSupport::SecureRandom ‘deprecated’ in Rails 3.1. Dit betekent dat er een waarschuwing wordt gegeven als je dit gebruikt en dat het in een latere versie zal worden verwijderd. SecureRandom zit namelijk standaard in Ruby 1.9, waardoor je geen Rails meer nodig hebt om deze functie te gebruiken.
Nadat de salt is gemaakt kunnen we het wachtwoord een hash geven. Hiervoor gebruiken we een SHA1 hash. Dit is een Ruby Class die je kunt gebruiken. Als je een error krijgt in de trant van dat Ruby ‘Digest’ niet kan vinden, moet je het volgende boven de Class neerzetten:
require 'digest/sha1'
Als je Ruby 1.9 gebruikt hoort dit geen probleem te zijn. Zoals je ziet wordt het wachtwoord samengevoegd met de salt voordat er een hash van wordt gemaakt. En aangezien dit in een before_save gedaan wordt zal dit ook in de database zo worden opgeslagen. Wat zijn callbacks toch handig. Wat we nu wel zijn vergeten is een attr_accessible toevoegen. We willen natuurlijk niet dat een gebruiker de admin kolom kan aanpassen door het formulier aan te passen. Voeg dit toe aan de User Model:
attr_accessible :email, :password, :password_confirmation
Nu kunnen alleen het e-mailadres, het wachtwoord en de bevestiging hiervan worden meegestuurd naar de ‘new’ en ‘update_attributes’ methods. Nu we de Model klaar hebben kunnen we beginnen aan de UsersController. Ik zal even snel drie acties hieraan toevoegen.
197 - Instappen in Ruby on Rails 3 - Robin Brouwer
class Admin::UsersController < Admin::BaseController def index @users = User.all end def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to([:admin, :users], notice: "User has been created.") else render("new") end end end
Een vrij standaard begin voor een CRUD. Laten we de Views hiervoor aanmaken. Deze moeten natuurlijk in de admin namespace worden gestopt. Ik geef bij de hekjes (#) aan in welke bestanden je de code moet stoppen.
# /app/views/admin/users/index.html.erb
Users overview
<% @users.each do |user| %> <%= div_for(user) do %> <%= user.email %> <% end %> <% end %>
<%= link_to("Create new User", [:new, :admin, :user]) %>
# /app/views/admin/users/new.html.erb
<%= back_to([:admin, :users]) %>
<%= render("form") %>
198 - Instappen in Ruby on Rails 3 - Robin Brouwer
# /app/views/admin/users/_form.html.erb <%= form_for([:admin, @user]) do |f| %>
<%= f.label(:email) %>
<%= f.text_field(:email) %>
<%= f.label(:password) %>
<%= f.password_field(:password) %>
<%= f.label(:password_confirmation) %>
<%= f.password_field(:password_confirmation) %>
<%= f.submit %>
<% end %>
Zoals je ziet wordt bij elke url :admin ervoor gezet. Dit moet omdat we in de admin namespace zitten. Als je nu de server start en naar de volgende link gaat kun je gebruikers beheren:
http://localhost:3000/admin
De rest van de CRUD kun je zelf afmaken. Denk erom dat je in een namespace zit.
9.6.2 Inloggen Nu we een admin gedeelte hebben is het handig om dit gedeelte te beschermen. Er moet een inlogsysteem komen en alleen de admin mag bij de admin komen. Laten we eerst een standaard admin maken door middel van een seed. Open in de /db map het bestand ‘seeds.rb’ en stop het volgende erin: # Create admin user user = User.new(email: "
[email protected]", password: "admin", password_confirmation: "admin") user.admin = true user.save
We moeten niet vergeten dat attr_accessible ervoor heeft gezorgd dat we de admin kolom niet meer via de new method kunnen zetten. Vandaar dat we dit apart moeten opgeven. Nu kunnen we de seed uitvoeren om een admin te maken voor onze applicatie.
199 - Instappen in Ruby on Rails 3 - Robin Brouwer
rake db:seed
Laten we nu het inlog gedeelte maken. Hiervoor maken we een SessionsController.
rails g controller sessions
Hierin maken we een new en create actie voor het inloggen.
class SessionsController < ApplicationController def new end def create @user = User.authenticate(params[:email], params[:password]) if @user session[:user_id] = @user.id url = @user.admin ? [:admin, :root] : :root redirect_to(url, notice: "Logged in!") else flash.now.notice = "Invalid email or password." render("new") end end end
De create actie ziet er net iets anders uit dan je gewend bent. We roepen hierin een class method aan genaamd ‘authenticate’ die het inloggen zal regelen. Deze class method geeft een gebruiker terug als het inloggen is gelukt en zal niks teruggeven als er iets fout gaat. De if-statement doet een check hierop. Als het niet is gelukt wordt de new View opnieuw getoond en wordt er direct in de flash notice een bericht gestopt. Als het is gelukt zal de sessie worden aangemaakt en wordt de gebruiker naar de root gestuurd of naar de admin. De volgende stap is de authenticate method aanmaken. Stop deze in de User Model.
def self.authenticate(email, password) user = where("email = ?", email).first user if user && user.password_hash == User.hash_secret(password, user.password_salt) end
200 - Instappen in Ruby on Rails 3 - Robin Brouwer
Als je ‘has_secure_password’ gebruikt in Rails 3.1 wordt er automatisch een method toegevoegd aan de Model om authenticatie toe te voegen. Deze heet toevallig ook ‘authenticate’. Deze accepteert, in tegenstelling tot mijn voorbeeld, maar één argument. user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) ... end
Je moet eerst de gebruiker opzoeken d.m.v. het e-mail adres. Hierna kun je het wachtwoord meegeven aan de ‘authenticate’ method. Deze geeft true of false terug.
De gebruiker wordt eerst gezocht door middel van het e-mailadres. Als een gebruiker is gevonden en als het wachtwoord overeenkomt zal het gebruikersobject worden teruggegeven. Zo niet krijg je nil terug. Dit kunnen we testen in de console.
rails c user = User.authenticate("
[email protected]", "admin")
In de user variabele komt nu het object van de admin te zitten. Het is dus gelukt. Probeer maar eens een ander wachtwoord in te vullen en je zult zien dat dit niet werkt. De logica is nu in orde. Wat we nog moeten doen is de routes goedzetten en de new View maken voor de SessionsController. Eerst de routes.
get "logout" => "sessions#destroy", as: "logout" get "login" => "sessions#new", as: "login" resources :sessions
We maken named routes voor de new en destroy acties en zetten de resources voor de SessionsController. We kunnen nu de new View maken voor de SessionsController. We maken hier gebruik van de form_tag helper. We gebruiken hier namelijk niet een instantie van een Model en zullen niks opslaan in de database. We moeten daarom gebruikmaken van de text_field_tag en password_field_tag. Ook moeten we niet vergeten de waarde van een veld te zetten mocht dit nodig zijn.
201 - Instappen in Ruby on Rails 3 - Robin Brouwer
Log in
<%= form_tag(:sessions) do %>
<%= label_tag(:email) %>
<%= text_field_tag(:email, params[:email]) %>
<%= label_tag(:password) %>
<%= password_field_tag(:password) %>
<%= submit_tag("Log in") %>
<% end %>
De parameters die we meesturen kun je aanspreken met params[:email] en params[:password]. Als het inloggen is mislukt moet het e-mailadres nog wel ingevuld blijven. Vandaar dat we aan de text_field_tag de parameter meegeven. Dit zorgt ervoor dat de waarde van het veld wordt gezet. Als we nu naar /login gaan in de applicatie, kom je bij het inlogformulier. Als je dan inlogt wordt je of naar de root of naar de admin gestuurd. Probeer het maar een keertje. Nu we een ingelogde gebruiker hebben kunnen we de current_user method maken. Deze stoppen we in de ApplicationController.
class ApplicationController < ActionController::Base protect_from_forgery helper_method :current_user private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end end
Nu kunnen we deze method gebruiken in onze applicatie om te kijken of iemand is ingelogd. Laten we eerst een before_filter voor de gehele applicatie maken. Je kunt alleen gebruikmaken van ons systeem als je bent ingelogd. Stop dit in de ApplicationController.
202 - Instappen in Ruby on Rails 3 - Robin Brouwer
before_filter :authenticate_user private def authenticate_user if current_user.blank? redirect_to(:login, notice: "You have to log in first!") end end
We moeten niet vergeten om een skip_before_filter in de SessionsController te stoppen.
class SessionsController < ApplicationController skip_before_filter :authenticate_user, only: [:new, :create] ... end
Als we nu naar de root van onze applicatie gaan en we zijn niet ingelogd, dan worden we teruggestuurd naar de inlog pagina. Het volgende wat we doen is de admin beveiligen, zodat alleen de admin daar kan komen. Dit doen we in de BaseController.
class Admin::BaseController < ApplicationController before_filter :authorize_admin private def authorize_admin unless current_user.admin redirect_to(:root, notice: "You are not allowed here!") end end end
Probeer nu maar eens in te loggen met een niet-admin account en naar het admin gedeelte te gaan. Je zult zien dat het niet zal werken. Het laatste wat we nog moeten doen is een een knop maken voor het uitloggen. Deze willen we alleen laten zien als de gebruiker is ingelogd. Laten we eerst de destroy actie maken in de SessionsController.
203 - Instappen in Ruby on Rails 3 - Robin Brouwer
def destroy session[:user_id] = nil redirect_to(:login, notice: "Logged out!") end
In de application layout stoppen we het volgende:
<% if current_user %>
<%= link_to("Logout", :logout) %>
<% end %> <%= yield %>
Als de gebruiker nu inlogt zal deze knop tevoorschijn komen. We hebben nu een heel simpel systeem gemaakt om in te loggen. Je kunt dit uiteraard zo complex maken als je wilt. Zo kun je gebruikers laten registreren, een ‘remember me’ functie erin stoppen en een ‘wachtwoord vergeten’ optie toevoegen. Ook zou het zo kunnen zijn dat een gebruiker alleen hoeft in te loggen om een comment te plaatsen. Dat ligt geheel aan jouw applicatie. Bij onze applicatie willen we het op deze manier hebben, zodat we alleen to-do’s kunnen laten zien die voor de gebruiker zijn.
9.6.3 To-do’s koppelen aan gebruikers We zullen er nu voor zorgen dat to-do’s worden gekoppeld aan een gebruiker. Als een gebruiker inlogt krijg hij of zij alleen zelfgemaakte to-do’s te zien. Het eerste wat we moeten doen is een ‘user_id’ kolom toevoegen aan de todos tabel. Je kunt dit ook bij de comments tabel doen, maar aangezien een comment altijd aan een to-do is gekoppeld hoeft dit niet per se. Je weet immers al welke gebruiker de to-do heeft aangemaakt. Als je echter wilt dat verschillende mensen comments kunnen plaatsen op een to-do, dan wil je dit natuurlijk wel in de comments tabel stoppen. Bij onze applicatie is dit echter niet nodig. We maken eerst een nieuwe migration. rails g migration add_user_id_to_todos
We gebruiken een omschrijvende naam voor de migration en stoppen het volgende erin:
204 - Instappen in Ruby on Rails 3 - Robin Brouwer
class AddUserIdToTodos < ActiveRecord::Migration def self.up add_column :todos, :user_id, :integer end def self.down remove_column :todos, :user_id end end
Voer de migration uit. Ik ga ervan uit dat je dit nu wel kunt. De volgende stap is de relatie leggen in de Models.
# user.rb class User < ActiveRecord::Base has_many :todos ... end # todo.rb class Todo < ActiveRecord::Base belongs_to :user ... end
Nu moeten we de index actie van de TodosController aanpassen. We willen alleen de todo’s van de gebruiker laten zien.
def index @todos = current_user.todos.order_by_completed end
Om een to-do daadwerkelijk te koppelen aan de gebruiker moeten we de create actie iets aanpassen. Dit kan op twee manieren:
205 - Instappen in Ruby on Rails 3 - Robin Brouwer
def create @todo = Todo.new(params[:todo]) if current_user.todos << @todo redirect_to(@todo, notice: "To-do has been created.") else render("new") end end
Of:
def create @todo = Todo.new(params[:todo]) @todo.user = current_user if @todo.save redirect_to(@todo, notice: "To-do has been created.") else render("new") end end
Beide mogelijkheden doen precies hetzelfde. De eerste is iets korter, dus laten we die maar gebruiken. Je zou de gebruiker overigens ook mee kunnen geven aan de new method. Dit doe ik hier niet, omdat ik dit niet via het formulier wil laten gaan. Laten we dit gelijk extra beveiligen in de Todo Model.
attr_accessible :title, :completed
Alle to-do’s die nu worden gemaakt zullen aan een gebruiker worden gekoppeld. Deze gebruiker krijgt alleen deze to-do’s te zien en kan er comments op plaatsen. We hebben echter nog één probleem. Een andere gebruiker kan een to-do van iemand anders bekijken door de url aan te passen. Hier hebben we weer een before_filter nodig. Dit moet op elke actie waar een to-do wordt opgehaald.
206 - Instappen in Ruby on Rails 3 - Robin Brouwer
before_filter :get_todo, except: [:index, :new, :create] private def get_todo @todo = Todo.find(params[:id]) if @todo.user != current_user redirect_to(:root, notice: "You are not allowed to see this To-do.") end end
Vergeet niet om bij alle acties de find method weg te halen. Dit doen we nu in de before_filter. Als een gebruiker nu via de url naar een to-do gaat die niet van hem/haar is, dan wordt de gebruiker teruggestuurd naar de root van de applicatie. Het hele gebruikersgedeelte van de applicatie is nu klaar voor gebruik! Vergeet niet om de railscast te bekijken die ik in het vorige gedeelte liet zien. Hierin wordt het maken van een inlogsysteem nog een keer heel duidelijk uitgelegd.
9.7 Samenvatting Gefeliciteerd, je hebt je eerste echte Ruby on Rails applicatie gemaakt! Je hebt gezien hoe je alles wat je hebt geleerd kunt samenvoegen om een Rails applicatie te maken waarin gebruikers to-do’s kunnen beheren en waar een admin gebruikers kan beheren. Ook weet je hoe je een simpel inlogsysteem kunt maken. Het is een vrij simpele applicatie geworden, maar het is een heel goed begin. Je kent nu officieel de basis van Rails en hebt dit in de praktijk tot uiting kunnen brengen. In de volgende hoofdstukken gaan we alles een beetje tweaken en zal ik wat extra dingen laten zien. We duiken iets dieper in Rails.
207 - Instappen in Ruby on Rails 3 - Robin Brouwer
10. Unobtrusive JavaScript Nu je een simpele Rails applicatie kunt maken kunnen we verder gaan met de wat diepgaandere dingen binnen Rails. Hetgeen waar we het dit hoofdstuk over zullen hebben is iets wat niet Rails-specifiek is. Het is echter ontzettend belangrijk voor bijna elke webapplicatie: JavaScript. Het is dé client-side script-taal voor het web en bijna elke webapplicatie maakt hier gebruik van. Het wordt gebruikt om AJAX requests uit te voeren, maar ook om leuke effecten in webapplicaties te stoppen. Denk aan een to-do lijstje waarin je to-do’s kunt verslepen om van plaats te laten verwisselen. Er zijn een aantal JavaScript frameworks die het werken met JavaScript een stuk eenvoudiger maken. Je hoeft hiermee veel minder te doen om veel voor elkaar te krijgen. Het framework wat standaard in Rails wordt gebruikt is Prototype. Dit werkt behoorlijk goed en je kunt er behoorlijk veel mee voor elkaar krijgen. Naast Prototype heb je het populaire jQuery. Dit werkt iets gemakkelijker dan Prototype en heeft een stuk meer plugins die je kunt gebruiken. Zoals je hebt gezien in de vorige hoofdstukken gebruik ik liever jQuery dan Prototype. Naast deze twee frameworks heb je nog een aantal andere frameworks die je kunt gebruiken. Zo heb je ook iets genaamd MooTools. Ik heb hier nooit mee gewerkt en weet dus niet hoe dit staat ten opzichte van jQuery. In dit hoofdstuk werken we met jQuery.
In Rails 3.1 is het standaard JavaScript framework jQuery geworden.
Ik zal in de eerste twee onderdelen van dit hoofdstuk een aantal voorbeelden geven. Je hoeft niet per se mee te doen met deze voorbeelden. Je hoeft pas bij 10.3 mee te doen. Dan gaan we namelijk onze applicatie aanpassen.
10.1 Unobtrusive Je zult nu misschien wel denken: dit hoofdstuk heet toch geen JavaScript? Dat klopt, dit hoofdstuk gaat over ‘Unobtrusive’ JavaScript. Vaak wordt JavaScript ‘inline’ in de HTML gestopt, in bijvoorbeeld een onclick attribuut. Dit kan ervoor zorgen dat je HTML volzit met allemaal JavaScript, die per pagina opnieuw moet worden ingeladen. Dit zorgt ervoor dat de HTML groter wordt, onoverzichtelijker, minder flexibel en dat het laden van je pagina’s net iets trager gaat. Dat is precies waar Unobtrusive JavaScript tegenover staat. Je stopt geen JavaScript in de HTML, maar verwijst naar een extern JavaScript bestand waarin alles wordt geregeld. In dit bestand check je of een bepaald DOM element op de pagina is en op basis hiervan kun je allerlei events koppelen aan de pagina. De HTML is een stuk kleiner, je webapplicatie wordt een stuk flexibeler en het JavaScript bestand wordt steeds uit de cache gehaald,
208 - Instappen in Ruby on Rails 3 - Robin Brouwer
waardoor je niet per pagina opnieuw de JavaScript moet inladen. Je webapplicatie werkt net iets soepeler en pagina’s laden sneller. Unobtrusive JavaScript is ook iets om ervoor te zorgen dat je applicatie werkt als iemand JavaScript uit heeft staan. Als je een JavaScript event hebt staan op een link waarmee je een AJAX request uitvoert, maar de gebruiker heeft JavaScript uitstaan, dan kun je met UJS er makkelijker voor zorgen dat de link toch naar de juiste url verwijst. Je maakt eerst je applicatie zonder JavaScript en kunt daarna gemakkelijk JavaScript toevoegen vanuit een extern bestand. Zo werkt je applicatie ook als iemand geen JavaScript gebruikt. Of je dit wilt is een heel andere discussie. Zo heeft een applicatie als Basecamp een grote waarschuwing voor iedereen die zonder JavaScript de applicatie probeert te gebruiken. Voor veel belangrijke functionaliteiten wordt namelijk JavaScript gebruikt. Je kunt je applicatie ook laten werken zonder JavaScript, maar je kunt ook degenen die het uit hebben staan weren van de site. Het is sowieso altijd belangrijk om niet te overdreven om te gaan met JavaScript. Je moet het zien als iets extra’s waarmee je applicatie net iets beter wordt en niet iets wat je overal in je applicatie stopt. Het kan wel, maar dan moet je wel een goede reden hebben. Om te illustreren hoe Unobtrusive JavaScript werkt zal ik een klein voorbeeld laten zien. Ik zal hiervoor jQuery gebruiken. Stel we hebben de volgende HTML:
UJS Test <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/ jquery/1.6.2/jquery.min.js"> <script type="text/javascript" src="application.js">
Klik hier!
De link in deze pagina verwijst naar een andere HTML pagina. Als je er nu op klikt zal je naar deze pagina gaan. Om hier een JavaScript event op te zetten moet je eerst het volgende in de application.js stoppen:
$(function(){ });
209 - Instappen in Ruby on Rails 3 - Robin Brouwer
Dit is iets binnen jQuery waarmee je kunt checken of de DOM geladen is. Hierbinnen kunnen we dan een click-event op de link plaatsen.
$(function(){ $("#andere_pagina").click(function(evt){ alert("CLICKED!"); evt.preventDefault(); }); });
Je spreekt het DOM element aan en roept de click functie erop aan. Hierin kun je een functie opgeven met wat er moet gebeuren. Belangrijk hierbij is ‘evt.preventDefault()’. Dit zorgt ervoor dat het standaard gedrag van een link niet wordt uitgevoerd na de JavaScript. Als je nu meerdere pagina’s hebt en je wilt alleen iets uitvoeren als je op een bepaalde pagina bent kun je een if-statement gebruiken. Stel we hebben op onze pagina een
met als id ‘page1’. Op deze pagina komt specifieke HTML die gebruikt moet worden in het JavaScript bestand. Hier willen we een check op doen. Dit werkt als volgt:
$(function(){ if($("#page1").length) { $("#andere_pagina").click(function(evt){ alert("CLICKED!"); evt.preventDefault(); }); } });
Zo wordt bij ‘page2’ de JavaScript niet uitgevoerd en kun je per pagina regelen wat er moet gebeuren qua JavaScript. Zo simpel is het dus om aan Unobtrusive JavaScript te doen. Rails heeft hier overigens een heel handige manier voor. Het rails.js bestand in onze applicatie is om ervoor te zorgen dat je heel gemakkelijk AJAX links en formulieren kunt maken. Hier in het volgende gedeelte meer over.
10.2 Rails en UJS In onze applicatie zit zoals je weet een rails.js bestand. Dit bestand gebruikt jQuery als framework. Als je een ander framework wilt gebruiken moet je het juiste rails.js bestand hiervoor vinden. Als je naar het bestand kijkt zie je een heleboel JavaScript erin. Er wordt echter niet gekeken naar DOM elementen met een bepaalde class of id, maar met een ‘data-’ attribuut. Dit is iets nieuws binnen HTML5 en zorgt ervoor dat je elke HTML tag een uniek attribuut kunt meegeven. Na ‘data-’ komt een willekeurig woord, waardoor er
210 - Instappen in Ruby on Rails 3 - Robin Brouwer
een zelfgemaakte attribuut ontstaat. Deze attribuut kun je een waarde geven en aanspreken met JavaScript. Stel we hebben de volgende link:
Confirm!
We hebben een attribuut genaamd ‘data-confirm’ met hierin een bepaalde waarde. We kunnen de volgende UJS gebruiken om deze link aan te spreken:
$('a[data-confirm]').click(function(e){ return confirm($(this).attr('data-confirm')); });
Nu zal er eerst een confirm-box komen voordat je naar de link wordt gestuurd. En dit allemaal zonder JavaScript te gebruiken in de HTML. Naast ‘click’ heb je ook iets genaamd ‘live’. Bij de live functie wordt het event steeds opnieuw geplaatst, waardoor na een update aan de pagina de events nog steeds bestaan. Bij een simpel click event zal deze verdwijnen als het DOM-element wordt overschreven en hierna wordt teruggezet. Bij de live functie is dit niet het geval en komt het event weer terug. Vandaar dat je in het rails.js bestand steeds ‘live’ ziet staan. Het is een stuk flexibeler.
Het rails.js bestand is in Rails 3.1 standaard toegevoegd aan application.js. Je ziet hierin het volgende staan: //= require jquery_ujs
Dit zorgt ervoor dat dit bestand wordt ingeladen.
Zoals je weet kun je in Rails de helper ‘link_to’ gebruiken om een link te maken. Dit is altijd een gewone link, zonder enige JavaScript. De JavaScript hiervoor kun je uiteraard zelf regelen, maar doordat je een rails.js bestand hebt kun je erg gemakkelijk een AJAX link maken van de link_to helper. Je gebruikt hiervoor de :remote key.
<%= link_to("Edit product", [:edit, product], remote: true) %>
211 - Instappen in Ruby on Rails 3 - Robin Brouwer
De volgende HTML zal worden gegenereerd.
Edit product
Er wordt automatisch een data-remote attribuut toegevoegd aan de link. De link zal een live click event meekrijgen vanuit het rails.js bestand, waardoor het een AJAX link wordt. AJAX staat voor Asynchronous JavaScript and XML en zorgt ervoor dat je gedeeltes van je pagina kunt updaten, zonder een nieuwe pagina in te laden. Je kunt een request naar de server sturen en vanaf de server bepaalde JavaScript terugsturen, zodat de pagina zonder een hele page-reload wordt aangepast. Je kunt dus heel gemakkelijk nieuwe data uit de database halen en laten zien, zonder de hele pagina opnieuw te tonen. Het zorgt voor een stuk minder stress voor de server en maakt de webapplicaties van tegenwoordig mogelijk. Het zorgt er namelijk voor dat webapplicaties meer op desktop applicaties lijken. Nu je weet wat AJAX is kunnen we verder gaan met het voorbeeld. Als er op de link wordt geklikt zal er een request naar de server worden gestuurd zonder dat een nieuwe pagina wordt geladen. De request zal naar dezelfde Controller actie gaan als bij een normale request: de edit actie van de ProductsController. Maar hoe zorg je er dan voor dat er JavaScript wordt teruggegeven? Dat is in Rails 3 verdomde simpel. Rails weet automatisch wanneer er een AJAX request wordt verstuurd en zal zoeken naar een ‘.js.erb’ View. In ons geval wordt er gezocht naar ‘edit.js.erb’ in de /app/views/products map. Dit is een bestand waarin je JavaScript kunt gebruiken én Ruby code kunt uitvoeren binnen de welbekende Ruby-tags. Dit werkt precies hetzelfde als de html.erb bestanden. Het grote verschil is dus dat er nu JavaScript moet komen. Wij zouden het volgende kunnen doen:
$('#product_1').html('<%= escape_javascript(render("form")) %>');
In Rails 3.1 kun je escape_javascript aanspreken met ‘j’. Er is dus een gemakkelijkere manier gekomen om JavaScript te escapen.
Je voert jQuery uit om de HTML in een bepaald DOM object te vervangen. Je gebruikt een Ruby-tag om een partial in te laden en gebruikt de ‘escape_javascript’ helper om ervoor te zorgen dat er geen errors komen. Dat is ook gelijk wat er zo handig is aan partials. Je kunt een gehele partial heel makkelijk via JavaScript in de pagina stoppen. Naast het DRY houden van je Views is dat een heel belangrijke functie van partials.
212 - Instappen in Ruby on Rails 3 - Robin Brouwer
Naast het inladen van een partial kun je nog veel meer binnen het JavaScript ERB bestand. Zo kun je alle JavaScript functies en variabelen gebruiken die zijn ingeladen. De speciale JavaScript Views zorgen er ook voor dat je gemakkelijker AJAX requests en gewone requests kunt scheiden. Voor de edit actie kun je ook een HTML View maken die dan wordt getoond als er geen AJAX request is. Rails weet welke View moet worden getoond. Je hoeft daarom ook geen respond_to te gebruiken in de Controller. Als je JavaScript wilt uitvoeren tijdens de AJAX request moet je JavaScript toevoegen aan application.js. Een AJAX request gebeurt namelijk niet gelijk, omdat je moet wachten totdat de server reageert. Je kunt het volgende doen om een ‘loading’ GIF te laten zien op de plaats van de AJAX link:
$("a[data-remote]").bind("ajax:loading", function(){ $(this).html('
![](/images/loader.gif)
'); });
In nieuwere versies van jQuery wordt er gebruikgemaakt van andere AJAX events. Zo zal ‘ajax:loading’ waarschijnlijk niet werken. Je kunt ‘beforeSend’ gebruiken om hetzelfde resultaat te krijgen.
Je voegt aan de data-remote links een ‘ajax:loading’ event toe die bij het laden een handige GIF laat zien op de plaats van de link. Dit is jQuery en heeft niet echt iets met Rails te maken. Je kunt hier dus van alles mee doen. Zo kun je ook een bepaalde AJAX link een class meegeven en deze een ‘ajax:loading’ event meegeven waar iets speciaals gebeurt. Dat is geheel aan jou. De nieuwe manier van AJAX requests in Rails is een welkome verbetering. In Rails 2.3 werd er gebruikgemaakt van speciale ‘link_to_remote’ helpers die inline JavaScript genereerde. Hiernaast gebruikte je vreemde ‘rjs’ templates om JavaScript te genereren. De nieuwe manier is een stuk logischer en ook nog een stuk flexibeler. UJS binnen Rails 3 zorgt er ook voor dat je heel gemakkelijk een andere HTTP method kunt gebruiken bij een link en een confirm-box kunt laten zien. Hiervoor heb je de :method en :confirm keys.
<%= link_to("Delete product", product, method: :delete, confirm: "Are you sure?") %>
Dit geeft dan de volgende HTML: 213 - Instappen in Ruby on Rails 3 - Robin Brouwer
Delete product
De HTTP method DELETE wordt nu gebruikt en je krijgt eerst een confirm-box te zien. Dit is weer iets wat het rails.js bestand voor je regelt. Naast de link_to helper kun je ook de :remote, :method en :confirm keys gebruiken bij een form_for of form_tag helper. Dit werkt precies hetzelfde als bij link_to en wordt geïnitieerd als op de submit knop wordt gedrukt. UJS in Rails 3 is ontzettend handig en kan je applicatie erg verrijken. Zorg er echter niet voor dat je applicatie er van afhankelijk is, tenzij je een hele goede reden hebt hiervoor. In het volgende gedeelte gaan we onze applicatie verrijken met wat Unobtrusive JavaScript. Als je meer wilt weten over UJS in Rails 3 kun je de Railscast hierover bekijken: Railscasts - Unobtrusive JavaScript http://railscasts.com/episodes/205-unobtrusive-javascript (TinyURL: http://tinyurl.com/y5exhaw)
10.3 In onze applicatie stoppen Nu je weet wat UJS is en hoe je een AJAX link kunt maken, gaan we dit in de praktijk tot uiting brengen door het in onze applicatie te stoppen. Als eerste maken we de edit actie wat netter door dit met AJAX te regelen. Hierna maken we gebruik van AJAX checkboxes om het afvinken wat gemakkelijker te maken.
10.3.1 Aanpassen met AJAX Pak als eerste de index View van de TodosController erbij en focus op het volgende: <%= div_for(todo, class: todo.completed ? "completed" : nil) do %> <%= link_to(todo.title, todo) %> <% end %>
We gaan hier een edit link aan toevoegen die met AJAX zal werken. Aangezien we met ‘div_for’ werken zal er om elke to-do een
worden geplaatst met een uniek id. Dit kunnen we later erg goed gebruiken. Eerst de edit link:
214 - Instappen in Ruby on Rails 3 - Robin Brouwer
<%= div_for(todo, class: todo.completed ? "completed" : nil) do %> <%= link_to(todo.title, todo) %> | <%= link_to("Edit", [:edit, todo], remote: true) %> <% end %>
Het volgende is de ‘edit.js.erb’ toevoegen. Hier laden we de form partial in de
.
$("#todo_<%= @todo.id %>").html('<%= escape_javascript(render("form")) %>');
Nu wordt het formulier in de
geplaatst en kun je gemakkelijk de to-do aanpassen. Het probleem is echter dat het formulier een gewone request uitvoert. Dit willen we ook via AJAX laten gaan. Dit kunnen we doen door een nieuwe form partial aan te maken, maar we kunnen natuurlijk ook het formulier aanpassen. Probleem hiermee is echter dat de ‘new’ actie ook gebruikmaakt van dit formulier. En deze actie gebruikt geen AJAX. Hetzelfde geldt voor de edit link bij de show View. Die willen we een gewone request laten uitvoeren. Je kunt dus voor elke AJAX request een aparte partial maken, maar dat is natuurlijk niet DRY. Wij gaan dit anders oplossen.
<%= form_for(@todo, remote: request.xhr?) do |f| %> ... <% end %>
We gebruiken ‘request.xhr?’ om te kijken of het een AJAX request is. Als het een gewone request is zal er false worden teruggegeven, waardoor de new actie een gewone request zal gebruiken bij hetzelfde formulier. Het volgende wat we moeten doen is update.js.erb toevoegen. Eerst moeten we een partial hebben voor een enkele to-do. Hiervoor maken we een partial genaamd ‘todo’ aan. We stoppen het volgende erin:
<%= div_for(todo, class: todo.completed ? "completed" : nil) do %> <%= link_to(todo.title, todo) %> | <%= link_to("Edit", [:edit, todo], remote: true) %> <% end %>
De index View kunnen we dan naar het volgende aanpassen om alles DRY te houden:
215 - Instappen in Ruby on Rails 3 - Robin Brouwer
To-do overview
<%= render(@todos) %>
<%= link_to("Create new To-do", [:new, :todo]) %>
Nu wordt de todo partial meerdere keren getoond en krijgen we hetzelfde resultaat. Het handige is dat we nu deze partial kunnen gebruiken in update.js.erb.
$("#todo_<%= @todo.id %>").html('<%= escape_javascript(render(@todo)) %>');
Er is echter nog één ding die we moeten regelen. De update actie voert namelijk een redirect_to uit. Als je de update actie met AJAX aanspreekt zal er achter de schermen een redirect worden uitgevoerd. Dit wil je natuurlijk niet. Dit kun je op twee manieren oplossen.
def update if @todo.update_attributes(params[:todo]) respond_to do |format| format.js format.html do redirect_to(@todo, notice: "To-do has been updated.") end end else render("edit") end end
Of:
def update if @todo.update_attributes(params[:todo]) unless request.xhr? redirect_to(@todo, notice: "To-do has been updated.") end else render("edit") end end
216 - Instappen in Ruby on Rails 3 - Robin Brouwer
Welke je gebruikt is geheel aan jou. Je hoeft trouwens niks speciaals te doen als het opslaan van de to-do mislukt. De render method zal namelijk de edit.js.erb View gebruiken als er een AJAX request wordt gedaan. Bij een gewone request zal de HTML View worden gebruikt. Het enige wat je dus moet afvangen is een redirect_to. Probeer nu maar eens een to-do aan te passen via de index actie. Dit gaat nu allemaal met AJAX en werkt perfect. Als je nu JavaScript uitzet zul je nog steeds een to-do kunnen aanpassen. Wat we nog missen is een cancel link in het formulier. Deze moet alleen worden getoond bij een AJAX request en moet de to-do weer laten zien. Eerst maken we de link in het formulier:
<%= form_for(@todo, remote: request.xhr?) do |f| %>
<%= f.label(:title, "To-do:") %> <%= f.text_field(:title) %> <%= f.submit %> <% if request.xhr? %> or <%= link_to("cancel", @todo, remote: true) %> <% end %>
<% end %>
Nu moeten we voor de show actie een JavaScript View aanmaken. Wat hier echter zal gebeuren is precies hetzelfde als bij de update actie. We laten de to-do simpelweg zien in een bepaalde
. Wat we daarom doen is update.js.erb hernoemen naar show.js.erb. Nu zal de cancel knop werken, maar het updaten niet meer. Hiervoor moeten we de update actie aanpassen.
def update if @todo.update_attributes(params[:todo]) respond_to do |format| format.js { render("show") } format.html do redirect_to(@todo, notice: "To-do has been updated.") end end else render("edit") end end
217 - Instappen in Ruby on Rails 3 - Robin Brouwer
En dat is alles. Nu hebben we ook een cancel knop en hebben we de update JavaScript View een logischere naam gegeven. De AJAX edit functionaliteit is klaar en we kunnen doorgaan met het afvinken van to-do’s.
10.3.2 Afvinken met AJAX Voor het afvinken zullen we in dit voorbeeld gebruikmaken van een normale checkbox. Wat je namelijk kunt doen is per to-do een AJAX formulier maken met een checkbox erin die automatisch wordt verstuurd als er op de checkbox wordt geklikt. Hiervoor moet je een click-event op de checkbox stoppen die het formulier submit. Dit formulier kan dan gewoon naar de update actie worden verstuurd, zodat je niet een speciale complete en incomplete actie nodig hebt. Je kunt natuurlijk een gewone link gebruiken, een checkbox als plaatje gebruiken en deze naar de complete en incomplete actie verwijzen. Mijn optie is echter iets leuker. Nadeel is dat het niet zonder JavaScript gaat werken. Laten we de todo partial erbij pakken en een formulier erin stoppen. <%= div_for(todo, class: todo.completed ? "completed" : nil) do %> <%= form_for(todo, remote: true) do |f| %> <%= f.check_box(:completed) %> <% end %> <%= link_to(todo.title, todo) %> | <%= link_to("Edit", [:edit, todo], remote: true) %> <% end %>
We moeten niet vergeten om de stijl van het formulier aan te passen, zodat het geen block element meer is: form { display: inline; }
Het volgende wat we moeten doen is een class meegeven, zodat we per checkbox JavaScript kunnen uitvoeren. <%= f.check_box(:completed, class: "check_todo") %>
Nu kunnen we het volgende in onze JavaScript stoppen:
218 - Instappen in Ruby on Rails 3 - Robin Brouwer
$(function(){ if($(".check_todo").length) { $(".check_todo").live("change", function(){ $(this).closest("form").submit(); // Zonder jQuery: this.form.submit(); }); } });
Elke keer als er nu op de checkbox wordt geklikt zal het formulier worden verstuurd. Aangezien dit steeds naar de update actie gaat, zal dit allemaal perfect verlopen. Het nadeel is echter dat de afgevinkte to-do’s niet meteen onderaan komen te staan. Dit gebeurt pas na een refresh. Je kunt dit op verschillende manieren oplossen. Ten eerste kun je ervoor zorgen dat alle to-do’s opnieuw worden opgehaald bij elke update. Dit doen we in show.js.erb.
$("#todos").html('<%= escape_javascript(render(current_user.todos.order_by_completed)) %>');
Nu worden alle to-do’s opnieuw opgehaald en in de #todos
gestopt. Het nadeel hiervan is dat alle to-do’s steeds moeten worden opgehaald en dat als je een andere to-do aan het aanpassen was het formulier verdwijnt. Het voordeel is dat de volgorde helemaal goed is en dat nieuwe to-do’s ook gelijk worden opgehaald. Een andere manier is om JavaScript te gebruiken om de afgevinkte to-do te positioneren. Je kunt de show.js.erb laten zoals het eerst was, maar moet wel iets extra’s eraan toevoegen. Het zorgt misschien niet voor exact dezelfde volgorde, maar kan ermee door.
var todo = $("#todo_<%= @todo.id %>"); todo.html('<%= escape_javascript(render(@todo)) %>'); <% if @todo.completed %> $("#todos").append(todo); <% else %> $("#todos").prepend(todo); <% end %>
Nu wordt een afgevinkte taak altijd aan het einde gestopt en een taak die weer actief wordt zal helemaal bovenaan komen te staan. Dit kan je natuurlijk helemaal aanpassen met jQuery om ervoor te zorgen dat de to-do niet helemaal onderaan komt, maar bijvoorbeeld wordt gesorteerd op naam. Dat is geheel aan jou.
219 - Instappen in Ruby on Rails 3 - Robin Brouwer
Zoals je ziet kun je nu heel gemakkelijk taken afvinken met behulp van AJAX. Het nadeel hiervan is dat de to-do niet wordt afgevinkt als je JavaScript uit hebt staan. Dit zou wel gebeuren als je er een gewone link van had gemaakt. Dat is dan ook precies de afweging die je moet maken. Wanneer laat je iets werken zonder JavaScript en wanneer niet. Dat ligt aan jou en de gebruiker die je voor ogen hebt. Je kunt natuurlijk altijd een <noscript> tag gebruiken om een waarschuwing te geven aan de gebruiker, zoals bij Basecamp gebeurt. Het is altijd handig om een callback te hebben voor de JavaScript requests, maar het zou niet een obstakel moeten vormen. Bijna iedereen heeft tegenwoordig JavaScript aanstaan, dus een heel groot probleem zou het niet zijn.
10.4 Samenvatting Je weet nu hoe je AJAX functionaliteit kunt toevoegen aan je applicatie en wat er nou precies wordt bedoeld met Unobtrusive JavaScript. Je zal vanaf nu geen inline JavaScript meer gebruiken en ervoor zorgen dat alles extern wordt geladen. Je weet ook hoe Rails omgaat met AJAX requests en hoe je AJAX terug kunt sturen naar de gebruiker. Genoeg geleerd dus! Hier nog op een rijtje wat je in dit hoofdstuk hebt geleerd: - Wat UJS nou precies is; - Wat AJAX precies is; - Hoe je UJS kunt gebruiken; - Hoe je data- attributen kunt gebruiken; - Hoe je een AJAX link en formulier maakt; - Hoe je AJAX requests afhandelt; - Hoe je JavaScript terugstuurt vanaf de server; - Hoe je een applicatie kunt verrijken met JavaScript.
220 - Instappen in Ruby on Rails 3 - Robin Brouwer
11. Locales Lokalisatie is iets wat behoorlijk belangrijk is bij een internationale webapplicatie. Je ondersteunt vaak standaard de Engelse taal, maar wilt misschien ook andere talen ondersteunen. Het zou behoorlijk handig zijn als je applicatie ook in het Nederlands te bekijken is. Het probleem is echter dat we nu alle teksten in de Views hebben staan. Als we hier ook Nederlandse vertalingen van willen hebben moeten we per tekst een if-statement gebruiken. Je krijgt dan ontzettend lelijke Views waarin je niet bepaald gemakkelijk een nieuwe taal kan introduceren. Gelukkig heeft Rails hier een goede oplossing voor: locales. Dit zijn bestanden waarin je alle statische tekst kwijt kunt per taal. Dit zijn YAML bestanden (.yml) die worden opgeslagen in /config/locales. Als je bij onze applicatie kijkt zie je een bestand genaamd ‘en.yml’ staan. Hierin kun je Engelse teksten stoppen. Als je dan ‘nl.yml’ toevoegt en dezelfde teksten vertaalt naar het Nederlands, kun je erg gemakkelijk deze vertalingen gebruiken. In de View kun je dan aangeven welke tekst je wilt gebruiken en Rails haalt de tekst uit dit bestand op basis van welke taal is geselecteerd. Het is de bedoeling dat je statische teksten altijd, maar dan ook echt altijd, in een locale bewaart. Dat is dan ook de reden waarom je geen :message moet gebruiken bij validatie in de Model. Dit kun je via een locale automatisch regelen. Hoe dit precies allemaal in zijn werk gaat zal ik in dit hoofdstuk uitleggen. Het is hierna aan jou om dit in onze applicatie uit te voeren.
11.1 Hiërarchie van locales Als je en.yml opent zie je het volgende: en: hello: "Hello world"
De Engelse vertaling van de locale ‘hello’ zal ‘Hello world’ worden. Hier kunnen we meer vertalingen aan toevoegen: en: hello: "Hello world" yesterday: "Yesterday" today: "Today" tomorrow: "Tomorrow"
Dit zijn globale locales en kun je heel gemakkelijk aanspreken in je applicatie. Hoe dit precies moet zal ik in het volgende gedeelte uitleggen. Wat voor nu belangrijk is, is hoe je
221 - Instappen in Ruby on Rails 3 - Robin Brouwer
een andere taal toevoegt. Dat is vrij simpel. Voeg een bestand genaamd ‘nl.yml’ toe aan de locales map. Hier kunnen we dan het volgende in stoppen om het te vertalen:
nl: hello: "Hallo wereld" yesterday: "Gisteren" today: "Vandaag" tomorrow: "Morgen"
Hierbij moet je opletten dat je ‘nl’ bovenaan zet in plaats van ‘en’. Als de taal dan op Nederlands staat zal dit bestand worden gebruikt in plaats van en.yml. Wat je misschien al opvalt is dat alle vertalingen onder ‘nl’ en ‘en’ genest zijn. Dat is ook hoe je een YAML bestand opbouwt. Je gebruikt twee spaties of een tab om iets te nesten. Dit werkt hetzelfde als bij de HAML Views die je kunt maken. Ook zie je dat een soort Hash syntax wordt gebruikt, maar dan zonder komma’s. Waar je overigens soms mee te maken krijgt is dat een bepaalde locale niet wordt herkend. Als je bijvoorbeeld het woordje ‘on’ wilt toevoegen moet dit op de volgende manier:
en: "on": "on"
Je moet dan aanhalingstekens eromheen doen. Dit is bij een aantal woorden zo. Hiernaast is het ook handig om aanhalingstekens om iets als ‘en-US’ te stoppen, omdat er een streepje tussenstaat.
"en-US": ...
Naast YAML te gebruiken kun je ook gebruikmaken van Ruby Hashes in een Ruby bestand.
222 - Instappen in Ruby on Rails 3 - Robin Brouwer
# /config/locales/nl.rb { nl: { hello: "Hallo wereld", yesterday: "Gisteren", today: "Vandaag", tomorrow: "Morgen" } }
Ik vind het handiger om YAML hiervoor te gebruiken. Ik ga daarom hiermee verder. Zoals ik aangaf kun je heel gemakkelijk verschillende vertalingen nesten. Het verschil zit in hoe je die vertalingen kunt aanroepen. Een vertaling die genest is, is net iets lastiger aan te roepen.
nl: notices: error: "Er is iets fout gegaan" alert: "Kijk uit!"
In Rails zijn een heleboel vertalingen al voor je genest, waardoor je gemakkelijk standaarden kunt overschrijven. Zo heb ik het al gehad over de :message key bij validatie in de Model. Dit kun je beter in een locale regelen. Deze zitten namelijk genest en kun je overschrijven in de locales. In Rails zijn voor zowel ActiveRecord als ActiveSupport aan aantal locales die je kunt overschrijven: ActiveRecord locales https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/ en.yml (TinyURL: http://tinyurl.com/46v9u7p) ActiveSupport locales https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/ en.yml (TinyURL: http://tinyurl.com/4p7fto8) Als je naar de ActiveRecord link gaat zie je hoe je bepaalde error messages kunt overschrijven. Je kunt dit globaal doen voor alle Models, maar ook specifiek voor een Model. Hier een voorbeeld hoe we bij de Todo Model ervoor kunnen zorgen dat er een ander bericht komt als er geen titel is ingevuld: 223 - Instappen in Ruby on Rails 3 - Robin Brouwer
en: activerecord: errors: models: todo: attributes: title: blank: "You need to fill in a description for this To-do."
Een heleboel geneste locales. Rails zal nu dit bericht gebruiken als de titel leeg is. Ook kun je ervoor zorgen dat per formulier de submit knop een bepaalde label krijgt.
en: helpers: submit: todo: create: "Add To-do" update: "Update To-do"
Nu wordt de submit knop bij een to-do automatisch gevuld met deze teksten en kun je het gemakkelijk vertalen. Bekijk de links hierboven om meer van dit soort voorbeelden te vinden. Vaak wil je per View een aantal locales tot je beschikking hebben. Hiervoor kun je de locales op een speciale manier nesten:
en: todos: index: overview: "To-do overview" new_todo: "Create new To-do"
Eerst geef je de Controller op en hierna de actie. Hierin kun je dan allerlei locales zetten. Nu kun je heel gemakkelijk deze locales aanspreken vanuit de View. Hoe dit werkt leg ik zometeen uit. Rails haalt automatisch alle bestanden in de locales map op. Je kunt daarom ook iets anders dan ‘en.yml’ gebruiken als bestandsnaam. Alle locales voor de Views zou je bijvoorbeeld in een ‘views.en.yml’ bestand kunnen stoppen. Rails laadt deze toch in. Als je het laden van locales wilt aanpassen kun je naar /config/application.rb kijken. Hierin staat het volgende: 224 - Instappen in Ruby on Rails 3 - Robin Brouwer
# The default locale is :en and all translations from config/locales/ # *.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*. {rb,yml}').to_s] # config.i18n.default_locale = :de
Zoals je ziet kun je de standaard locale hier zetten en de load_path aanpassen. Zo kun je ook mappen aanmaken in de locales map om alles mooi te structureren. Denk aan de volgende structuur voor de View locales:
locales views todos en.yml nl.yml users en.yml nl.yml
Dan komen alle locales geordend in deze YAML bestanden te staan. Vergeet deze mappen dan niet toe te voegen aan de load_path. Je kunt hiervoor het volgende gebruiken:
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*. {rb,yml}')]
Vergeet niet dat als je een nieuw bestand toevoegt de server opnieuw moet worden gestart. De locales worden namelijk maar één keer ingeladen: bij het starten van de server. Je kunt wel dingen binnen deze bestanden veranderen zonder de server opnieuw op te starten. Nu je weet hoe je locales kunt aanmaken kunnen we doorgaan met het aanroepen van de locales.
11.2 Locales aanroepen Het aanroepen van een locale hangt af van hoe deze is genest. Een aantal specifiek geneste locales worden door Rails automatisch weergegevens, zoals de labels en foutmeldingen uit het vorige gedeelte. Dit regelt Rails en hoef jij niks extra’s voor te doen. Globale locales zijn vrij simpel aan te spreken. Om een locale te laten zien gebruik je de ‘t’ (translate) method. Deze kun je in de Controller en View als volgt aanroepen:
225 - Instappen in Ruby on Rails 3 - Robin Brouwer
t(:hello)
Dit werkt niet in de Model. Hiervoor moet je het volgende doen:
I18n.t(:hello)
Nu wordt de tekst getoond die in het locales bestand staat. Welke tekst wordt getoond is weer afhankelijk van welke taal is geselecteerd. Hoe je een taal kunt selecteren laat ik zien in het volgende gedeelte van dit hoofdstuk. Wat iets lastiger is om aan te roepen zijn geneste locales. Het kan op verschillende manieren en werkt als volgt bij ons ‘notices’ voorbeeld van het vorige gedeelte:
t(:alert, scope: [:notices]) t('notices.alert')
De :scope key zorgt ervoor dat je een Array mee kunt sturen waarin staat hoe de locale genest is. Hiernaast kun je een String meegeven en met punten aangeven waar de locale zich precies bevind. De twee voorbeelden hierboven geven dus hetzelfde resultaat. Het valt dus nog mee hoe je deze geneste locales kunt aanspreken. Het zal er alleen raar uitzien als je het ontzettend diep gaat nesten en het op deze manier aanspreekt. Als je ergens diep in gaat nesten is het handig om een helper hiervoor te maken. Ook kun je een standaardwaarde meegeven. Als de locale dan niet kan worden gevonden krijg je dat te zien.
t(:missing, default: 'Missing') t(:missing, default: :also_missing) t(:missing, default: [:also_missing, 'Missing'])
Je kunt aan de :default key meegeven wat de String is die moet worden getoond of de locale die dan moet worden gebruikt. Als je een Array meegeeft kun je aangeven waar eerst naar moeten worden gezocht. Zo laten we bij voorbeeld drie eerst also_missing zien. Als also_missing niet bestaat krijgen we de String te zien. Om de speciale View locales te laten zien kun je iets genaamd ‘Lazy Lookup’ gebruiken. Op de volgende manier kunnen we onze index View voor de to-do’s naar het volgende vertalen: 226 - Instappen in Ruby on Rails 3 - Robin Brouwer
<%= t(".overview") %>
<%= render(@todos) %>
<%= link_to(t(".new_todo"), [:new, :todo]) %>
Je geeft een String mee aan de translate method en begint hierin met een punt. Rails weet dan dat hij moet zoeken naar ‘todos.index’ en laat de juiste tekst zien. Probeer dit nu in de rest van de applicatie te stoppen, compleet met Nederlandse vertalingen. Bedenk ook goed wanneer je View specifieke locales nodig hebt en wanneer niet. Een ‘back’ knop komt vaker voor en kun je misschien beter als globale locale gebruiken. Stop ook gelijk alle View specifieke locales in ‘views.en.yml’ en ‘views.nl.yml’. Of je kunt zoals het voorbeeld in het vorige gedeelte alles ordenen in mappen. Dat is geheel aan jou. Het laatste wat ik over de locales wil zeggen is hoe je een variabele kunt meesturen naar een locale. Als eerste moet je de locale opgeven. Hierin kun je %{} gebruiken om aan te geven waar de variabele moet komen. Denk erom: het is een procent-teken. Dit is anders dan bij een gewone Ruby String.
en: hello: "Hello %{name}"
Je kunt dan het volgende doen om een naam mee te sturen:
t(:hello, name: "Robin")
Dat is dus ook vrij gemakkelijk om voor elkaar te krijgen. Je kunt hierdoor de locales een heel stuk flexibeler maken. Je kunt naast de dingen die ik tot nu toe heb opgenoemd nog veel meer met locales. Alles hierover kun je in de Ruby on Rails guide vinden. Een link hiernaartoe geef ik aan het einde van dit hoofdstuk. Laten we nu maar verder gaan met het daadwerkelijk veranderen van de taal in de applicatie, zodat je ziet hoe gemakkelijk het is om je applicatie te lokaliseren.
11.3 Verwisselen van taal In Rails wordt er standaard gebruikgemaakt van de Engelse taal. Om de taal te veranderen kun je het volgende uitvoeren: 227 - Instappen in Ruby on Rails 3 - Robin Brouwer
I18n.locale = :nl
Elke request die je doet zal zoeken naar de en.yml bestanden en hier de vertalingen uit halen. Als je bij een request de taal verandert zal bij de volgende request alles weer in het Engels zijn. Je moet daarom de taal onthouden. Dit kun je in een sessie doen, maar is niet aan te raden doordat dit nogal tegen het hele REST gebeuren ingaat. Je kunt dit beter via de url regelen. De gebruiker weet dan ook precies welke taal wordt gebruikt en zou dit zelfs via de url kunnen aanpassen. Wat ik nu ga laten zien is de meest-gebruikte manier van lokalisatie binnen Rails. Eerst moeten we een before_filter toevoegen aan de Application Controller. Deze zet elke keer de lokalisatie voor de request.
before_filter :set_locale private def set_locale I18n.locale = params[:locale] end
Bij elke request zal de taal worden omgezet naar de locale die met de link wordt meegestuurd. Het enige lastige punt hieraan is dat elke link een extra parameter moet meesturen. Om dit nou bij elke link te doen is nogal onpraktisch. Laten we dit zo aanpassen dat dit automatisch gebeurt. Dit kunnen we ook in de Application Controller regelen. We gebruiken hiervoor de default_url_options method waarmee we de url_for helper kunnen aanpassen die bij alle paden wordt gebruikt.
private def default_url_options(options={}) { locale: I18n.locale } end
Bij elke link zal nu de huidige locale worden meegestuurd. Het nadeel hiervan is dat je nu de volgende url’s krijgt:
228 - Instappen in Ruby on Rails 3 - Robin Brouwer
/todos?locale=nl
Het zou veel mooier zijn als de locale niet aan het einde zou zitten, maar er als volgt uit zou zien:
/nl/todos
Om dit voor elkaar te krijgen moeten we iets aanpassen in de routing. We gebruiken hiervoor een scope waarin we al onze resources stoppen.
TodoManager::Application.routes.draw do scope ":locale" do ... end root to: "todos#index" end
Alles wat in routes.rb staat moet je op de plaats van de drie puntjes neerzetten, behalve de root van de applicatie. Die moet je buiten de scope zetten (er is namelijk één root). Als je ervoor wilt zorgen dat er niet per se een locale meegestuurd hoeft te worden kun je het volgende doen:
scope "(:locale)", locale: /en|nl/ do ... end
Je stopt haakjes eromheen om ervoor te zorgen dat de locale optioneel is. Als je nu naar de url /todos/1 gaat wordt de Engelse vertaling gebruikt, maar zie je dit niet in de url. Je krijgt nu ook geen RoutingError, wat ook best handig kan zijn. Je moet er ook nog voor zorgen dat als je direct naar de locale (/en of /nl) gaat je wel naar de juiste pagina wordt verwezen.
match ":locale" => "todos#index", as: "locale"
Je moet nu wel uitkijken dat je niet de root_path aanspreekt om terug te gaan naar de root. Anders krijg je namelijk weer die lelijke url met de locale erachter. De root kan geen locale 229 - Instappen in Ruby on Rails 3 - Robin Brouwer
ervoor krijgen zoals bij de scope. Het is namelijk de root. Gebruik daarom nu locale_path om naar de root te gaan van de locale. Als het goed is werkt de lokalisatie in je applicatie nu helemaal perfect. Creëer een aantal nieuwe YAML bestanden voor verschillende talen en verander de url een aantal keer. Je zult zien dat de taal daadwerkelijk wordt aangepast. Best handig die vertalingen. Naast het gebruiken van een scope kun je ook de locale uit de domeinnaam halen. Hoe dit precies werkt kun je zien in de Ruby on Rails guide. Ook zit in de guide veel extra informatie over locales binnen Rails. Rails Internationalization (I18n) API http://guides.rubyonrails.org/i18n.html Misschien vraag je je nu af wat voor talen er nou precies allemaal zijn en wat de afkortingen zijn. Gelukkig hoef je dit niet zelf te verzinnen en is er altijd wel een handige website die dit soort dingen voor je klaar heeft staan. Zo heeft iemand een I18n plugin gemaakt waarin de standaard Rails vertalingen in alle andere talen zijn vertaald. Hier een overzicht van al deze locales: Github - rails-i18n plugin https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale (TinyURL: http://tinyurl.com/4lgtttg)
11.4 Samenvatting Je weet nu hoe je een internationale applicatie kunt lokaliseren naar andere talen. Je gaat vanaf nu alle teksten die je in een applicatie stopt in een locale stoppen, zodat je later heel gemakkelijk alles kunt vertalen. Het is dan namelijk een kwestie van de locale vertalen. Zelfs mensen zonder technische kennis zouden dat kunnen doen. Hier nog op een rijtje wat je in dit hoofdstuk hebt geleerd: - Wat locales precies zijn en wat het nut ervan is; - Dat je alle teksten in locales moet stoppen; - Hoe je locales kunt aanmaken en nesten; - Hoe je View specifieke locales kunt aanmaken; - Hoe je locales kunt aanspreken; - Hoe je locales kunt structureren in mappen; - Hoe je de taal van je applicatie kunt aanpassen; - Hoe je d.m.v. de url de taal kunt onthouden; - Hoe je scope kunt gebruiken om de taal te onthouden.
230 - Instappen in Ruby on Rails 3 - Robin Brouwer
12. Mailer De Mailer was in Rails 2.3 het meest rampzalige onderdeel van Rails. Het was totaal onlogisch in elkaar gezet en werkte nou niet bepaald geweldig. Ik zag er altijd tegenop om een Mailer te maken. In Rails 3 heeft de Mailer een grote make-over gekregen en werkt het een heel stuk beter dan eerst. Het is nu leuk om mails te versturen vanuit Rails.
12.1 Mailer aanmaken Wij gaan voor onze applicatie een Mailer maken die een mail stuurt naar het e-mailadres van een nieuwe gebruiker. In dit mailtje krijgt de gebruiker zijn of haar wachtwoord om in te loggen. Als eerste moet de Mailer worden gegenereerd. Hiervoor heb je een rails script die je kunt gebruiken. Dit werkt hetzelfde als bij een Model. rails g mailer user_mailer
De conventie bij een Mailer is om eerst een omschrijving te geven wat voor Mailer het is gevolgd door een liggend streepje en het woord ‘mailer’. Aangezien wij een mail versturen naar een gebruiker noemen wij onze Mailer ‘user_mailer’. Bij het genereren wordt in de map /app/mailers een nieuw bestand toegevoegd en krijg je een nieuwe map in /app/views genaamd user_mailer. Hier komen de Views voor de mails die we zullen maken. Het nieuwe bestand in de mailers map is de Mailer. Deze ziet er als volgt uit: class UserMailer < ActionMailer::Base default :from => "
[email protected]" end
In de mailer kun je een standaardwaarde meegeven voor waar de e-mail vandaan moet worden gestuurd. Meestal is dit een ‘noreply’ adres. Laten we dit gelijk aanpassen. class UserMailer < ActionMailer::Base default from: "
[email protected]" end
231 - Instappen in Ruby on Rails 3 - Robin Brouwer
Nu zullen alle mailtjes standaard vanuit dit adres worden gestuurd. Dit kun je ook per mail actie regelen, maar meestal is het zo dat er alleen uit één adres wordt gestuurd.
12.2 Acties aanmaken De volgende stap is het maken van de Mailer acties. Dit zijn acties zoals in de Controller die een speciale View laten zien. Het verschil is dat deze View wordt verstuurd per mail. Hiernaast moet je wat extra dingen doen in de Mailer actie om naar het juiste adres te mailen. Wij maken een actie die het wachtwoord verstuurt naar een gebruiker die is aangemaakt via de admin. Deze actie noemen we ‘registration’.
class UserMailer < ActionMailer::Base default from: "
[email protected]" def registration(user) @user = user end end
Je kunt naar deze actie een object sturen van een gebruiker. Deze stoppen we in een instance variabele, zodat we deze later in de View kunnen aanspreken (zoals je ook bij de Controller doet). Het volgende wat moet gebeuren is aangeven waar het mailtje naartoe moet gaan en wat het onderwerp is. Dit kunnen we aan de ‘mail’ method meegeven.
def registration(user) @user = user mail(to: user.email, subject: "Your To-Do Manager account has been created!") end
Deze mail method roep je aan het einde van de Mailer actie aan. Je geeft een Hash mee met informatie over de mail. Zo gebruiken we de :to key om aan te geven waar het mailtje naartoe moet en de :subject key om aan te geven wat het onderwerp moet zijn. Als je goed hebt opgelet bij het vorige hoofdstuk zie je dat we hier iets fout doen. Er is een tekst en die zetten we hier gelijk in de Mailer. Dit moet natuurlijk in een locale. Laten we dit een beetje structureren. Eerst de locale:
232 - Instappen in Ruby on Rails 3 - Robin Brouwer
en: user_mailer: registration: subject: "Your To-Do Manager account has been created!"
Deze locale kunnen we dan als volgt gebruiken binnen de Mailer actie:
def registration(user) @user = user @scope = [:user_mailer, :registration] mail(to: user.email, subject: t(:subject, scope: @scope)) end
We voegen een @scope variabele toe waarin we de scope voor de locale gemakkelijk kunnen opslaan. Omdat dit een instance variabele is kunnen we deze ook gebruiken voor de teksten in de mail. Het aanmaken van de Mail Views is vrij gemakkelijk. Dit werkt namelijk ongeveer hetzelfde als bij de Controller. Stop in /app/views/user_mailer een bestand genaamd ‘registration.html.erb’. Deze View zal dan worden gebruikt als HTML View. Vaak wordt er bij een e-mail ook een ‘plain text’ alternatief meegestuurd en dat gaan wij dus ook doen. Stop deze in dezelfde map en noem deze ‘registration.text.erb’. Rails zal zien dat je twee templates hebt voor de Mailer actie en zal beide templates versturen. Je hoeft niet een content type of iets dergelijks op te geven. Ook Mailer Views kunnen een layout gebruiken. Het verschil is dat je zowel een HTML als text layout nodig hebt en dat je de naam van de Mailer moet gebruiken. Stop de volgende twee bestanden in /app/views/layouts: user_mailer.html.erb en user_mailer.text.erb. Deze twee layouts zullen dan standaard gebruikt worden voor deze Mailer. Je kunt ook bovenaan de Mailer opgeven welke layout gebruikt moet worden.
class UserMailer < ActionMailer::Base layout "mailer" ... end
Zo kun je steeds dezelfde layout gebruiken voor alle Mailers in je applicatie. Laten we even snel een simpele layout maken voor onze actie. Eerst de HTML.
233 - Instappen in Ruby on Rails 3 - Robin Brouwer
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <%= yield %>
Best regards,
The To-Do Manager team
En hierna de tekst variant.
<%= yield %> Best regards, The To-Do Manager team
Zoals je ziet gebruiken we hier weer gewone tekst. Dit kun je nu in een locale stoppen. De volgende stap is het maken van de Views van onze actie. Eerst maken we de locales aan.
en: user_mailer: registration: subject: "Your To-Do Manager account has been created!" dear: "Dear %{name}," welcome: "Welcome to To-Do Manager! Your account has been created and you can log in by using the following password:" visit: "Visit the link below to login. Have fun creating To-Do's!"
Nu kunnen we deze locales gebruiken in onze Views:
234 - Instappen in Ruby on Rails 3 - Robin Brouwer
# HTML:
<%= t(:dear, scope: @scope, name: @user.name) %>
<%= t(:welcome, scope: @scope) %>
<strong><%= @user.password %>
<%= t(:visit, scope: @scope) %>
<%= link_to("http://todomanager.com", "http://todomanager.com") %>
# TEXT: <%= t(:dear, scope: @scope, name: @user.name) %> <%= t(:welcome, scope: @scope) %> ============ <%= @user.password %> ============ <%= t(:visit, scope: @scope) %> http://todomanager.com
Nu kunnen we heel gemakkelijk de e-mail vertalen naar een andere taal zonder te knoeien met de Mailer Views. Ik gebruik hier overigens ‘@user.name’ om de naam van de gebruiker op te halen. Als het goed is heb je geen naam van de gebruiker in de database staan, dus hier zou je een nieuwe kolom voor moeten maken. Ik ga er vanuit dat je weet hoe dit moet. Nu de Views klaar zijn kunnen we verder gaan met het mailen. Voordat ik hierop inga wil ik nog een paar links geven met extra informatie over de Mailer in Rails. Je kunt er namelijk nog veel meer mee dan dat ik hier heb laten zien. In de Ruby on Rails guide kun je vinden wat je zoal kunt doen met de ActionMailer. Het versturen van een bijlage is bijvoorbeeld ook erg gemakkelijk te regelen. Hiernaast is er een railscast over het versturen van mails in Rails 3 die je zeker moet bekijken. Action Mailer Basics http://guides.rubyonrails.org/action_mailer_basics.html (TinyURL: http://tinyurl.com/y8t7drk) Railscast - Action Mailer in Rails 3: http://railscasts.com/episodes/206-action-mailer-in-rails-3 (TinyURL: http://tinyurl.com/2365dcv)
12.3 Mail versturen Het volgende wat we moeten doen is de mail versturen. Het versturen van een mail werkt vrij simpel. Als eerste roep je de Mailer aan en hierna de actie.
235 - Instappen in Ruby on Rails 3 - Robin Brouwer
UserMailer.registration(User.first)
Hierna roep je simpelweg de deliver method aan en de mail zal worden verstuurd.
UserMailer.registration(User.first).deliver
De mail zal echter niet gelijk worden verstuurd, omdat we nog wat gegevens in de applicatie moeten aanpassen. Hier zometeen meer over. Eerst roepen we de Mailer actie aan in een callback in de User Model. Dit doen we in een after_create callback.
after_create Proc.new { |user| UserMailer.registration(user).deliver }
Nu zal de mail worden verstuurd als de gebruiker wordt opgeslagen. Aangezien het slechts één regel is die moet worden uitgevoerd heb ik dit in een Procedure gestopt.
12.4 Environment gegevens Voordat de mail daadwerkelijk verstuurd kan worden moeten we wat gegevens aanpassen. Deze gegevens kun je in /config/environments/development.rb stoppen als je in development mode zit. Ik vind het echter handiger om deze gegevens in een ‘initializer’ te stoppen. Dit is een Ruby script die je kunt uitvoeren als de server start. Deze stop je in de map /config/initializers. Maak hierin een bestand genaamd ‘setup_mail.rb’. Hier kunnen we onze gegevens voor de mail server stoppen. Zo kunnen we het volgende doen om ‘sendmail’ te gebruiken:
ActionMailer::Base.delivery_method = :sendmail ActionMailer::Base.perform_deliveries = true ActionMailer::Base.raise_delivery_errors = false
De delivery_method geeft aan welke methode je wilt gebruiken om mails te versturen. Zo kun je ook smtp gebruiken. De perform_deliveries method geeft aan of de e-mail moet worden verstuurd. Deze kan je uitzetten als je de e-mails niet wilt ontvangen. Je kunt dan in de log kijken hoe de e-mail eruit ziet. Dit is vooral handig als je in development de Mailer wilt testen, maar niet de mails wilt ontvangen. De raise_delivery_errors method zorgt ervoor dat er een error komt als er iets fout gaat bij het versturen. Dit is echter niet helemaal betrouwbaar en kan ervoor zorgen dat de applicatie crasht bij de gebruiker. Het kan handiger zijn om de gebruiker er niks van te laten merken, maar zelf wel een bericht ervan te krijgen. Hiernaast wacht Rails totdat de e236 - Instappen in Ruby on Rails 3 - Robin Brouwer
mail is verstuurd, waardoor de gebruiker lang moet wachten. Ik zet deze method daarom meestal op false. Als je tijdens development echter problemen hebt met het versturen van mails kan het handig zijn om deze tijdelijk op true te zetten. Dan zie je precies wat er fout gaat. Het versturen van e-mails zal nu niet opeens werken. Je moet namelijk wat extra dingen doen om sendmail lokaal aan de praat te krijgen. Op een productie server zou de e-mail wel gelijk kunnen worden verstuurd. Dit ligt aan hoe de server is ingesteld. Lokaal gebruik ik liever SMTP voor het versturen van e-mails. Hier een voorbeeld die gebruikmaakt van Gmail:
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { address: "smtp.gmail.com", port: 587, domain: "todomanager.com", user_name: "username", password: "password", authentication: "plain", enable_starttls_auto: true }
Hier kun je nu jouw SMTP gegevens invullen. De e-mails zullen dan worden verstuurd via deze SMTP gegevens. Bij 45north gebruiken we onze eigen SMTP server. Wij gebruiken hiervoor ongeveer de volgende gegevens:
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.smtp_settings = { address: "smtp.45north.nl", port: 25, domain: 45north.nl", user_name: "username", password: "password", authentication: :login, enable_starttls_auto: false } ActionMailer::Base.perform_deliveries = true ActionMailer::Base.raise_delivery_errors = false
Dat is dus net iets anders dan bij Gmail. Jouw SMTP gegevens kunnen ook iets anders in elkaar zitten. Als je het niet aan de praat krijgt kun je het beste raise_delivery_errors op true zetten en allerlei gegevens proberen aan te passen. Doe dit totdat het werkt. Kom je er dan nog niet uit dan kun je vast het antwoord op Google vinden.
237 - Instappen in Ruby on Rails 3 - Robin Brouwer
Als je nu als admin inlogt en een nieuwe gebruiker aanmaakt zal deze gebruiker onze email ontvangen. Gefeliciteerd, je hebt je eerste e-mail verzonden vanuit Rails!
12.5 Samenvatting Je kunt nu ook e-mails versturen via Rails. Je applicatie begint steeds meer op een echte webapplicatie te lijken. Het laatste wat nog ontbreekt is bestanden uploaden. Dit gaan we in het volgende hoofdstuk voor elkaar krijgen. Hier nog op een rijtje wat je in dit hoofdstuk hebt geleerd: - Een nieuwe Mailer aanmaken; - Mailer acties aanmaken; - Views creëren voor de Mailer acties; - Een layout maken voor de Mailer; - Hoe je een mail kunt versturen; - Hoe je de SMTP gegevens kunt instellen.
238 - Instappen in Ruby on Rails 3 - Robin Brouwer
13. Uploaden We zijn alweer bij het laatste hoofdstuk waarin we dieper in Rails duiken. We gaan het hebben over het uploaden van bestanden en hoe je dit het beste kunt doen in Rails. Zoals je gewend bent zal dit allemaal in de Model gebeuren. Hoe dit precies werkt zal ik je uitleggen. Ook zal ik het in het kort hebben over ImageMagick en Paperclip.
13.1 Formulier Het eerste wat moet gebeuren is het formulier klaarmaken om bestanden te versturen. We gaan bij het aanmaken van een nieuwe to-do de mogelijkheid geven om een bestand mee te sturen. Let erop dat je niet een bestand kunt uploaden via AJAX. Dit komt omdat het onmogelijk is om via JavaScript een bestand te uploaden. Daarom laten we de bestandselectie in het formulier niet zien als er AJAX wordt gebruikt. Je kunt wel een soort AJAX uploader maken door het formulier naar een iframe te sturen en de iframe bepaalde JavaScript terug te laten sturen naar de bovenliggende pagina. Ook zijn er een paar andere manieren om dit voor elkaar te krijgen. Dat ga ik hier echter niet behandelen. Het eerste wat moet gebeuren is een attr_accessor toevoegen aan de Model, die we kunnen aanspreken in het formulier. Deze noemen we asset. class Todo < ActiveRecord::Base attr_accessor :asset end
Vergeet niet om :asset ook toe te voegen aan attr_accessible. Nu kunnen we ons formulier aanpassen. We moeten hier nog iets extra’s toevoegen om een bestand te kunnen uploaden. <%= form_for(@todo, remote: request.xhr?, html: { multipart: true }) do |f| %>
De :multipart key zorgt ervoor dat er een extra attribuut wordt toegevoegd aan het formulier. Ons formulier ziet er als volgt uit: