Category Archives: Tutorials

Jede Menge Tutorials für Rails, Ruby und Webdeveloper

Ruby on Rails Volltextsuche mit ElasticSearch und Tire

Installation

OS X

Unter OS X kann man sich die aktuelle Version via homebrew (?)infoHelpHomebrew ist ein OpenSource Paketmanager für OS X installieren:
Bash Screenshot von der Installation auf OSX
Debian / Ubuntu
Eine Installationsanleitung für Debian basierte Systeme befindet sich direkt auf elasticsearch.org: http://www.elasticsearch.org/tutorials/2010/07/02/setting-up-elasticsearch-on-debian.html

RedHat / CentOS …
Für RedHat basierte Systeme hat Tavisto (https://github.com/tavisto/elasticsearch-rpms) entsprechende SPEC-Files für die ElasticSearch RPM Erstellung bereitgestellt.

andere Systeme
Alternativ kann die aktuelle Version hier heruntergeladen und installiert werden: http://www.elasticsearch.org/download/

Nach der Installation und dem Start des Services sollte der ElasticSearch Server erreichbar sein. Der ElasticSearch Server liefert den Status, seine Versionsnummer und weitere Zusatzinformationen als JSON zurück:
JSON-Dokument mit Installationsdetails

Zustand der ElasticSearch überwachen

Für einen späteren produktiven Einsatz der ElasticSearch sollte das Monitoring nicht außer Acht gelassen werden. Hierbei haben sich verschiedene Monitoring-Tools bewährt:

  • Elasticsearch-head mobz.github.com/elasticsearch-head
    Elasticsearch-head ist eine statische Webseite mit JavaScript. Hilft die Performance der einzelnen Shards (?)infoHelpengl. für Scherben zu überwachen. Man kann sich direkt ähnlich wie bei PHPMyAdmin oder Futon(CouchDB) direkt die Daten anzeigen lassen.
  • Kibana github.com/rashidkpc/Kibana
    Kibana ist eine eigene Rails-Anwendung und sehr benutzerfreundlich. Manuelles Suchen, komplette Visualisierungen und Analysen z.B. auf Basis der Logs können konfiguriert und dargestellt werden.
  • Bigdesk github.com/lukas-vlcek/bigdesk
    Bigdesk eignet sich gut für das Monitoring der Hauptparameter des ElasticSearch Servers (Speicher, CPU, …).

ElasticSearch Funktionsweise / Daten speichern

Angenommen wir möchten eine Suche für einen Blog anlegen. Der Blog besteht aus mehreren Artikeln. Jeder Artikel enthält einen Text, eine Überschrift, ein Datum, sowie einen User. Da ElasticSearch eine RESTful-HTTP Schnittstelle bereitstellt, können wir mit Hilfe von z.B. curl (?)infoHelpCurl ist ein Kommandozeilen-Programm zum Übertragen von Dateien im Netzwerk alle Artikeldatensätze im JSON-Format an die ElasticSearch übergeben. Im nachfolgenden Beispiel übermitteln wir zwei Artikel, samt Inhalt an die ElasticSearch.

curl -XPUT 'http://localhost:9200/artikel/text/1' -d '{
    "user" : "Alex",
    "post_date" : "2012-10-05T14:12:12",
    "ueberschrift" : "Alles wird schlechter",
    "text" : "Hamburg - Immer mehr Arbeitnehmer in Deutschland haben zwei Jobs..."}'
curl -XPUT 'http://localhost:9200/artikel/text/2' -d '{
    "user" : "Alex",
    "post_date" : "2012-10-04T14:12:12",
    "ueberschrift" : "96 gewinnt",
    "text" : "Hannover 96 - Der neue große HSV von 1896 schlägt Levante daheim in Unterzahl 2:1"}'

Jeder einzelne Request wird bestätigt:

{"ok":true,"_index":"artikel","_type":"text","_id":"1","_version":1}

Wir sehen, dass der Index automatisch von der ElasticSearch erstellt wird, falls dieser noch nicht existiert. In unserem Fall heißt der Index “artikel”. Jeder Index besteht aus mehreren sogenannten Shards(?)infoHelpengl. für Scherbe. Diese wiederum werden automatisch über die einzelnen Nodes (Server), falls vorhanden, verteilt. Standardmäßig hat jeder Index fünf primäre Shards (0-4). Die Anzahl der primären Shards können nach Erstellung des Index nicht mehr geändert werden.
Jede Shard kann null oder mehr Replica-Shards haben. Ein Replica-Shard ist eine Kopie einer primären Shard. Die Anzahl der Replica-Shards kann zur Laufzeit dynamisch geändert werden.
Die Hauptaufgabe der Replica-Shards sind das Failover Verhalten zu verbessern, falls die primäre Shard nicht mehr zur Verfügung steht und die Performance der Elasticsearch bei Get- und Search-Requests zu verbessern.

Architektur im Clusterverbund

Möchte man andere Parameter bei der Indexerstellung setzen, hilft einem die Reference Guide weiter (elasticsearch.org/guide/reference/api/admin-indices-create-index.html). Die ElasticSearch arbeitet mit Versionierung. D.h. laden wir unseren ersten Artikel erneut, z.B. mit korrigiertem Text hoch, so erhalten wir nachfolgende Artwort:

{"ok":true,"_index":"artikel","_type":"text","_id":"1","_version":2}

Anders als z.B. bei CouchDB oder anderen NoSQL-Datenbanken hat man bei ElasticSearch keinen Zugriff mehr auf eine ältere Version eines Dokuments.

ElasticSearch gespeicherte Daten ausgeben

Wir können uns, wie bei einer RESTful NoSQL-Datenbank zum Beispiel CouchDB, einen Eintrag zurückgeben lassen:

curl -XGET 'http://localhost:9200/artikel/text/1'
{
  "_index" : "artikel",
  "_type" : "text",
  "_id" : "1",
  "_version" : 1,
  "exists" : true, "_source" : {
    "user" : "Alex",
    "post_date" : "2012-10-05T14:12:12",
    "ueberschrift" : "Alles wird schlechter",
    "text" : "Hamburg - Immer mehr Arbeitnehmer in Deutschland haben zwei Jobs..."}
}

Datensätze suchen

Ausführliche Informationen findet man unter dem Stichwort Query-DSL sowie Search-API der Reference Guide. An dieser Stelle möchte ich lediglich beispielhaft alle Datensätze des Users Alex finden:

curl -XGET 'http://localhost:9200/artikel/_search?q=user:Alex'
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "artikel",
      "_type" : "text",
      "_id" : "1",
      "_score" : 1.0, "_source" : {
    "user" : "Alex",
    "post_date" : "2012-10-05T14:12:12",
    "ueberschrift" : "Alles wird schlechter",
    "text" : "Hamburg - Immer mehr Arbeitnehmer in Deutschland haben zwei Jobs..."}
    }, {
      "_index" : "artikel",
      "_type" : "text",
      "_id" : "2",
      "_score" : 1.0, "_source" : {
    "user" : "Alex",
    "post_date" : "2012-10-04T14:12:12",
    "ueberschrift" : "96 gewinnt",
    "text" : "Hannover 96 - Der neue große HSV von 1896 schlägt Levante daheim in Unterzahl 2:1"}
    } ]
  }
}

ElasticSearch in Ruby on Rails Anwendungen mit dem Gem Tire einbinden

Wir clonen uns nachfolgende Rails-Anwendung und initialisieren diese:

git clone https://github.com/agilastic/blog-search-with-tire-and-elasticsearch
cd blog-search-with-tire-and-elasticsearch
bundle install
bundle exec rake db:setup

Die “blog-search-with-tire-and-elasticsearch”-Railsanwendung ist sehr überschaubar gehalten. Via Scaffolding haben wir “Article” erzeugt.

rails generate scaffold Article author:string, content:text, tag:string, published_at:date, title:string

Durchsucht werden sollen die Inhalte unseres Models “Article”. Hierzu muss das Model “Article” angepasst werden. Um Tire einzubinden müssen im “Article”-Model zwei Module eingebunden werden. Das erste Modul dient zur Einstellung der Suche und der Indizierung. Das zweite Modul kümmert sich um die Callbacks und führt die automatische Updates des Index bei jede Änderung des Artikels aus. Außerdem muss ein Block für das Mapping definiert werden. Des Weiteren passen wir die search-Methode etwas an.

class Article < ActiveRecord::Base
  include Tire::Model::Search
  include Tire::Model::Callbacks
  attr_accessible :author, :tag, :content, :published_at, :title

  mapping do
    indexes :id,           :index    => :not_analyzed
    indexes :title,        :analyzer => 'snowball', :boost => 100
    indexes :content,      :analyzer => 'snowball'
    indexes :author,       :analyzer => 'keyword'
    indexes :tag,       	 :analyzer => 'keyword'
    indexes :published_at, :type => 'date', :include_in_all => false
  end


  def self.search(params)
    tire.search(load: true) do
      query {string params[:query]} if params[:query].present?
    end 
  end
end

Wer sich wundert was der “Snowball-Analyzer” ist, kann sich auf der Projekt-Webseite genauer informieren snowball.tartarus.org/texts/introduction.html
Der Schneeball Analyzer kommt von Lucene und wurde ursprünglich in einem Projekt von snowball.tartarus.org entwickelt.

Das Suchformular in der articles#index-View wird angelegt:

<div class="search">
  <%= form_tag articles_path, method: :get do %>
    <%= text_field_tag :query, params[:query] %>
    <%= submit_tag "Suchen", name: nil %>
  <% end %>
</div>

Abschließend können Suchanfragen im Google-Stil durchgeführt werden:
Browserscreenshot mit Auflistung der Artikel

Sollten bereits mehrere Artikel vor der Integration von Tire im Blog vorhanden sein oder der ElasticSearch-“Artikel”-Index korrupt sein, so kann dieser neu angelegt und reindeziert werden. Dazu kann beispielsweise der nachfolgende Raketask genutzt werden:

rake elasticsearch:recreate_index
namespace :elasticsearch do
  task :recreate_index => :environment do
    @data_tables = ActiveRecord::Base.connection.tables
    @data_tables.delete("schema_migrations")
    @data_tables.each do |single_table|
      model = single_table.capitalize.singularize.constantize
      model.all.each do |a|
        a.tire.update_index
      end
    end
  end

Viel Spaß beim Ausprobieren :)

Gastbeitrag von: Alexander Ebeling-Hoppe
Alexander Ebeling-Hoppe arbeitet bei der Zentralen Polizeidirektion Niedersachsen als Ruby on Rails, sowie als freiberuflicher Webentwickler (agilastic.de) und Lehrbeauftragter.
Werbung in eigener Sache:

Ich bieten Ihnen eine viertägige Ruby on Rails Grundlagenschulung bei der VHS Hannover (Büssingweg 9, 30165 Hannover) an:

27./28.10.2012, 09:00 Uhr – 16:00 Uhr (Sa/So)
03./04.11.2012, 09:00 Uhr – 16:00 Uhr (Sa/So)
Kursnummer: 54079D8
Gebühr: 156,40 € (regulär)
Schüler, Studenten, Azubis etc. 113,20 €

Weitere Details: VHS on RAILS

Screencast: Bildbearbeitung mit RMagick

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

brew install imagemagick
convert -version
convert octocat.png -resize '70x70^' -gravity center -crop '70x70+0+0' -quantize GRAY -colors 256 -contrast source.png
composite stamp_overlay.png source.png source.png
convert -size 70x70 canvas:red \( octocat.png -resize '70x70^' -gravity center -crop '70x70+0+0' -quantize GRAY -colors 256 -contrast stamp_overlay.png -composite -negate \) -compose copy-opacity -composite stamp.png
gem install rmagick
mate stamp.rb
ruby stamp.rb

stamp.rb


require "rmagick"

source = Magick::Image.read("octocat.png").first
source = source.resize_to_fill(70, 70).quantize(256, Magick::GRAYColorspace).contrast(true)
overlay = Magick::Image.read("stamp_overlay.png").first
source.composite!(overlay, 0, 0, Magick::OverCompositeOp)
colored = Magick::Image.new(70, 70) { self.background_color = "red" }
colored.composite!(source.negate, 0, 0, Magick::CopyOpacityCompositeOp)
colored.write("stamp.png")

# Or through the command line:
# system <<-COMMAND
# convert -size 70x70 canvas:red \\( octocat.png \
#   -resize '70x70^' -gravity center -crop '70x70+0+0' \
#   -quantize GRAY -colors 256 -contrast stamp_overlay.png \
#   -composite -negate \
# \\) -compose copy-opacity -composite stamp.png
# COMMAND

Gemfile

gem 'rmagick'
gem 'carrierwave'

models/stamp.rb


mount_uploader :image, StampUploader

app/uploaders/stamp_uploader.rb


class StampUploader < CarrierWave::Uploader::Base
  include CarrierWave::RMagick

  # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
  include Sprockets::Helpers::RailsHelper
  include Sprockets::Helpers::IsolatedHelper

  # Choose what kind of storage to use for this uploader:
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_white_list
    %w(jpg jpeg gif png)
  end

  %w[red green blue purple black].each do |color|
    version(color) { process stamp: color }
  end

  def stamp(color)
    manipulate! format: "png" do |source|
      overlay_path = Rails.root.join("app/assets/images/stamp_overlay.png")
      overlay = Magick::Image.read(overlay_path).first
      source = source.resize_to_fill(70, 70).quantize(256, Magick::GRAYColorspace).contrast(true)
      source.composite!(overlay, 0, 0, Magick::OverCompositeOp)
      colored = Magick::Image.new(70, 70) { self.background_color = color }
      colored.composite(source.negate, 0, 0, Magick::CopyOpacityCompositeOp)
    end
  end
end

stamps/index.html

<%= image_tag stamp.image_url(:blue) %>

Screencast: Bullet

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

rails g migration add_products_count_to_categories products_count:integer
rails g migration cache_products_count
rake db:migrate

Gemfile


gem 'bullet', group: :development

config/initializers/bullet.rb


if defined? Bullet
  Bullet.enable = true
  # Bullet.alert = true
  Bullet.bullet_logger = true
end

categories_controller.rb


def index
  @categories = Category.order(:name).includes(:products)
end

db/migrate/cache_products_count.rb


class CacheProductsCount < ActiveRecord::Migration
  def up
    execute "update categories set products_count=(select count(*) from products where category_id=categories.id)"
  end

  def down
  end
end

Screencast: Ransack

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

Gemfile


gem 'ransack'

products_controller.rb


def index
  @search = Product.search(params[:q])
  @products = @search.result
end

products/index.html.erb


<%= search_form_for @search, url: search_products_path, method: :post do |f| %>
  <%= f.condition_fields do |c| %>
    <%= render "condition_fields", f: c %>
  <% end %>
  <p><%= link_to_add_fields "Add Conditions", f, :condition %></p>
  <div class="field">
    Sort:
    <%= f.sort_fields do |s| %>
      <%= s.sort_select %>
    <% end %>
  </div>
  <div class="actions"><%= f.submit "Search" %></div>
<% end %>

<table id="products">
  <tr>
    <th><%= sort_link @search, :name, "Product Name" %></th>
    <th><%= sort_link @search, :released_on, "Release Date" %></th>
    <th><%= sort_link @search, :price, "Price" %></th>
  </tr>
<% @products.each do |product| %>
  <tr>
    <td><%= link_to(product.name, product) %></td>
    <td><%= product.released_on.strftime("%B %e, %Y") %></td>
    <td><%= number_to_currency(product.price) %></td>
  </tr>
<% end %>
</table>

config/routes.rb


resources :products do
  collection { post :search, to: 'products#index' }
end

products_controller.rb

def index
  @search = Product.search(params[:q])
  @products = @search.result
  @search.build_condition if @search.conditions.empty?
  @search.build_sort if @search.sorts.empty?
end

products/index.html.erb

<%= search_form_for @search, url: search_products_path, method: :post do |f| %>
  <%= f.condition_fields do |c| %>
    <%= render "condition_fields", f: c %>
  <% end %>
  <p><%= link_to_add_fields "Add Conditions", f, :condition %></p>
  <div class="field">
    Sort:
    <%= f.sort_fields do |s| %>
      <%= s.sort_select %>
    <% end %>
  </div>
  <div class="actions"><%= f.submit "Search" %></div>
<% end %>

products/_condition_fields.html.erb


<div class="field">
  <%= f.attribute_fields do |a| %>
    <%= a.attribute_select associations: [:category] %>
  <% end %>
  <%= f.predicate_select %>
  <%= f.value_fields do |v| %>
    <%= v.text_field :value %>
  <% end %>
  <%= link_to "remove", '#', class: "remove_fields" %>
</div>

application_helper.rb


def link_to_add_fields(name, f, type)
  new_object = f.object.send "build_#{type}"
  id = "new_#{type}"
  fields = f.send("#{type}_fields", new_object, child_index: id) do |builder|
    render(type.to_s + "_fields", f: builder)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

products.js.coffee

jQuery ->
  $('form').on 'click', '.remove_fields', (event) ->
    $(this).closest('.field').remove()
    event.preventDefault()

  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

Screencast: MiniProfiler

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

Gemfile


  gem 'rack-mini-profiler'

products/index.html.erb

<%= pluralize project.tasks.size, "task" %>

projects_controller.rb


@projects = Project.order(:created_at).select("projects.*, count(tasks.id) as tasks_count").joins("left outer join tasks on project_id=projects.id").group("projects.id")
Rack::MiniProfiler.step("fetch projects") do
  @projects.all
end

config/environments/production.rb


config.serve_static_assets = true

terminal

rake assets:precompile
rake db:setup RAILS_ENV=production
rails s -e production

application_controller.rb


before_filter :miniprofiler

private

def miniprofiler
  Rack::MiniProfiler.authorize_request # if user.admin?
end

Screencast: Sidekiq

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal


  brew install redis
redis-server /usr/local/etc/redis.conf
bundle exec sidekiq
bundle exec sidekiq -q high,5 default

Gemfile

gem 'sidekiq'
gem 'sinatra', require: false
gem 'slim'

snippets_controller.rb

PygmentsWorker.perform_async(@snippet.id)
# PygmentsWorker.perform_in(1.hour, @snippet.id)

app/workers/pygments_worker.rb


class PygmentsWorker
  include Sidekiq::Worker
  sidekiq_options queue: "high"
  # sidekiq_options retry: false
  
  def perform(snippet_id)
    snippet = Snippet.find(snippet_id)
    uri = URI.parse("http://pygments.appspot.com/")
    request = Net::HTTP.post_form(uri, lang: snippet.language, code: snippet.plain_code)
    snippet.update_attribute(:highlighted_code, request.body)
  end
end

routes.rb


require 'sidekiq/web'
# ...
mount Sidekiq::Web, at: '/sidekiq'

Screencast: Active Records Reputation System

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal


  rails g reputation_system
rake db:migrate

Gemfile


  gem 'activerecord-reputation-system', require: 'reputation_system'

models/haiku.rb


  has_reputation :votes, source: :user, aggregated_by: :sum

models/user.rb


  has_many :evaluations, class_name: "RSEvaluation", as: :source

has_reputation :votes, source: {reputation: :votes, of: :haikus}, aggregated_by: :sum

def voted_for?(haiku)
  evaluations.where(target_type: haiku.class, target_id: haiku.id).present?
end

config/routes.rb


  resources :haikus do
  member { post :vote }
end

haikus_controller.rb


  def index
  @haikus = Haiku.find_with_reputation(:votes, :all, order: "votes desc")
end

def vote
  value = params[:type] == "up" ? 1 : -1
  @haiku = Haiku.find(params[:id])
  @haiku.add_or_update_evaluation(:votes, value, current_user)
  redirect_to :back, notice: "Thank you for voting"
end

_haiku.html.erb


  | <%= pluralize haiku.reputation_value_for(:votes).to_i, "vote" %>
<% if current_user && !current_user.voted_for?(haiku) %>
  | <%= link_to "up", vote_haiku_path(haiku, type: "up"), method: "post" %>
  | <%= link_to "down", vote_haiku_path(haiku, type: "down"), method: "post" %>
<% end %>

application.html.erb


  <%= current_user.reputation_value_for(:votes).to_i %>

models/haiku_vote.rb


  validates_uniqueness_of :haiku_id, scope: :user_id
validates_inclusion_of :value, in: [1, -1]
validate :ensure_not_author

def ensure_not_author
  errors.add :user_id, "is the author of the haiku" if haiku.user_id == user_id
end

models/haiku.rb


  def self.by_votes
  select('haikus.*, coalesce(value, 0) as votes').
  joins('left join haiku_votes on haiku_id=haikus.id').
  order('votes desc')
end

def votes
  read_attribute(:votes) || haiku_votes.sum(:value)
end

models/user.rb


  def total_votes
  HaikuVote.joins(:haiku).where(haikus: {user_id: self.id}).sum('value')
end

def can_vote_for?(haiku)
  haiku_votes.build(value: 1, haiku: haiku).valid?
end

haikus_controller.rb


  def index
  @haikus = Haiku.by_votes
end

def vote
  vote = current_user.haiku_votes.new(value: params[:value], haiku_id: params[:id])
  if vote.save
    redirect_to :back, notice: "Thank you for voting."
  else
    redirect_to :back, alert: "Unable to vote, perhaps you already did."
  end
end

_haiku.html.erb


  | <%= pluralize haiku.votes, "vote" %>
<% if current_user && current_user.can_vote_for?(haiku) %>
  | <%= link_to "up", vote_haiku_path(haiku, value: 1), method: "post" %>
  | <%= link_to "down", vote_haiku_path(haiku, value: -1), method: "post" %>
<% end %>

application.html.erb


  <%= current_user.total_votes %>

Screencast: Datenexport nach Excel oder CSV

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

config/application.rb

require 'csv'

products_controller.rb

def index
  @products = Product.order(:name)
  respond_to do |format|
    format.html
    format.csv { send_data @products.to_csv }
    format.xls # { send_data @products.to_csv(col_sep: "\t") }
  end
end

models/product.rb

def self.to_csv(options = {})
  CSV.generate(options) do |csv|
    csv << column_names
    all.each do |product|
      csv << product.attributes.values_at(*column_names)
    end
  end
end

config/initializers/mime_types.rb

Mime::Type.register "application/xls", :xls

views/products/index.xls.erb

<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:x="urn:schemas-microsoft-com:office:excel"
  xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
  xmlns:html="http://www.w3.org/TR/REC-html40">
  <Worksheet ss:Name="Sheet1">
    <Table>
      <Row>
        <Cell><Data ss:Type="String">ID</Data></Cell>
        <Cell><Data ss:Type="String">Name</Data></Cell>
        <Cell><Data ss:Type="String">Release Date</Data></Cell>
        <Cell><Data ss:Type="String">Price</Data></Cell>
      </Row>
    <% @products.each do |product| %>
      <Row>
        <Cell><Data ss:Type="Number"><%= product.id %></Data></Cell>
        <Cell><Data ss:Type="String"><%= product.name %></Data></Cell>
        <Cell><Data ss:Type="String"><%= product.released_on %></Data></Cell>
        <Cell><Data ss:Type="Number"><%= product.price %></Data></Cell>
      </Row>
    <% end %>
    </Table>
  </Worksheet>
</Workbook>

Screencast: Authentifizierung über Facebook

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

Gemfile

gem 'omniauth-facebook'

config/initializers/omniauth.rb

OmniAuth.config.logger = Rails.logger

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_SECRET']
end

terminal

rails g model user provider uid name oauth_token oauth_expires_at:datetime
rake db:migrate

models/user.rb

def self.from_omniauth(auth)
  where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name
    user.oauth_token = auth.credentials.token
    user.oauth_expires_at = Time.at(auth.credentials.expires_at)
    user.save!
  end
end

config/routes.rb

match 'auth/:provider/callback', to: 'sessions#create'
match 'auth/failure', to: redirect('/')
match 'signout', to: 'sessions#destroy', as: 'signout'

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(env["omniauth.auth"])
    session[:user_id] = user.id
    redirect_to root_url
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url
  end
end

application_controller.rb

private

def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user

layouts/application.html.erb

<div id="user_nav">
  <% if current_user %>
    Signed in as <strong><%= current_user.name %></strong>!
    <%= link_to "Sign out", signout_path, id: "sign_out" %>
  <% else %>
    <%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %>
  <% end %>
</div>

app/assets/javascripts/facebook.js.coffee.erb


jQuery ->
  $('body').prepend('<div id="fb-root"></div>')

  $.ajax
    url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
    dataType: 'script'
    cache: true


window.fbAsyncInit = ->
  FB.init(appId: '<%= ENV["FACEBOOK_APP_ID"] %>', cookie: true)

  $('#sign_in').click (e) ->
    e.preventDefault()
    FB.login (response) ->
      window.location = '/auth/facebook/callback' if response.authResponse

  $('#sign_out').click (e) ->
    FB.getLoginStatus (response) ->
      FB.logout() if response.authResponse
    true

Screencast: Brakeman

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

gem install brakeman
rbenv rehash
brakeman
brakeman -o brakeman.html
bundle update rails
brakeman --rake

config/application.rb

config.active_record.whitelist_attributes = true

products_controller.rb

def index
  direction = params[:direction] == "desc" ? "desc" : "asc"
  @products = Product.order("name #{direction}")
end

sessions_controller.rb

redirect_to redirect_url, only_path: true

models/user.rb

validates_format_of :name, with: /\A\w+\z/

Gemfile

gem 'brakeman', group: :development

Screencast: Session Hijacking

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

sudo tcpdump -i lo0 -A
curl http://todo.dev/
curl http://todo.dev/ -H 'Cookies: ...'
curl https://todo.dev/ -k -H 'Cookies: ...'

config/environments/production.rb

config.force_ssl = true

sessions_controller.rb

cookies.signed[:secure_user_id] = {secure: true, value: "secure#{user.id}"}
# ...
cookies.delete(:secure_user_id)

application_controller.rb

def current_user
  if !request.ssl? || cookies.signed[:secure_user_id] == "secure#{session[:user_id]}"
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
end

Screencast: Squeel

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

rails g squeel:initializer
rails c

rails console

Product.where{released_at < 3.months.ago}
Product.where{released_at.lt 3.months.ago}
Product.where{released_at.lt(3.months.ago) & price.gt(20)}
Product.where{released_at.lt(3.months.ago) | price.gt(20)}

models/product.rb

def self.search(query)
  where do
    (released_at <= Time.zone.now) &
    ((discontinued_at == nil) | (discontinued_at > Time.zone.now)) &
    (stock >= my{low_stock}) & (name =~ "%#{query}%")
  end
end

def self.low_stock
  2
end

Screencast: APIs schützen

 

Downloads in verschiedenen Formaten:

mp4
mp4
webm
ogg

 

Resourcen:

terminal

rails g model api_key access_token
curl http://localhost:3000/api/products -I
curl http://localhost:3000/api/products -u 'admin:secret'
curl 'http://localhost:3000/api/products?access_token=123' -I
curl http://localhost:3000/api/products -H 'Authorization: Token token="c576f0136149a2e2d9127b3901015545"'

api/v1/products_controller.rb

# http_basic_authenticate_with name: "admin", password: "secret"
before_filter :restrict_access

private

# def restrict_access
#   api_key = ApiKey.find_by_access_token(params[:access_token])
#   head :unauthorized unless api_key
# end

def restrict_access
  authenticate_or_request_with_http_token do |token, options|
    ApiKey.exists?(access_token: token)
  end
end

models/api_key.rb

before_create :generate_access_token

private

def generate_access_token
  begin
    self.access_token = SecureRandom.hex
  end while self.class.exists?(access_token: access_token)
end

Screencast: Versionierung von REST APIs

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

rails g migration change_products_released_on
rake db:migrate
cp -R app/controllers/api/v1 app/controllers/api/v2
curl -H 'Accept: application/vnd.example.v1' http://localhost:3000/api/products

routes.rb

require 'api_constraints'

Store::Application.routes.draw do
  namespace :api, defaults: {format: 'json'} do
    scope module: :v1, constraints: ApiConstraints.new(version: 1) do
      resources :products
    end
    scope module: :v2, constraints: ApiConstraints.new(version: 2, default: true) do
      resources :products
    end
  end
  
  resources :products
  root to: 'products#index'
end

lib/api_constraints.rb

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.example.v#{@version}")
  end
end

app/controllers/api/v1/products_controller.rb

module Api
  module V1
    class ProductsController < ApplicationController
      class Product < ::Product
        # Note: this does not take into consideration the create/update actions for changing released_on
        def as_json(options = {})
          super.merge(released_on: released_at.to_date)
        end
      end
      
      respond_to :json

      def index
        respond_with Product.all
      end

      def show
        respond_with Product.find(params[:id])
      end

      def create
        respond_with Product.create(params[:product])
      end

      def update
        respond_with Product.update(params[:id], params[:product])
      end

      def destroy
        respond_with Product.destroy(params[:id])
      end
    end
  end
end

Screencast: Das Rails API Gem

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

gem install rails-api
rbenv rehash
rails-api new todo
rails g scaffold task name
rake middleware
diff api_middleware.txt full_middleware.txt

tasks_controller.rb

include ActionController::MimeResponds
include ActionController::Helpers
include ActionController::Cookies
include ActionController::ImplicitRender

# GET /tasks
# GET /tasks.json
def index
  @tasks = Task.all

  # respond_to do |format|
  #   format.json { render }
  #   format.xml { render xml: @tasks }
  # end
end

views/tasks/index.json.rabl

collection @tasks 
attributes :id, :name

config/application.rb

config.middleware.insert_after ActiveRecord::QueryCache, ActionDispatch::Cookies

Screencast: Mehrseitige Formulare mit Wicked

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

rails g controller user_steps

user_steps_controller.rb

class UserStepsController < ApplicationController
  include Wicked::Wizard
  steps :personal, :social
  
  def show
    @user = current_user
    render_wizard
  end
  
  def update
    @user = current_user
    @user.attributes = params[:user]
    render_wizard @user
  end
  
private

  def redirect_to_finish_wizard
    redirect_to root_url, notice: "Thank you for signing up."
  end
end

user_steps/personal.html.erb

<h1>Tell us a little about yourself.</h1>

<%= render layout: 'form' do |f| %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :date_of_birth %><br />
    <%= f.date_select :date_of_birth, start_year: 1900, end_year: Date.today.year, include_blank: true %>
  </div>
  <div class="field">
    <%= f.label :bio %><br />
    <%= f.text_area :bio, rows: 5 %>
  </div>
<% end %>

user_steps/social.html.erb

<h1>Where can we find you?</h1>

<%= render layout: 'form' do |f| %>
  <div class="field">
    <%= f.label :twitter_username %><br />
    <%= f.text_field :twitter_username %>
  </div>
  <div class="field">
    <%= f.label :github_username %><br />
    <%= f.text_field :github_username %>
  </div>
  <div class="field">
    <%= f.label :website %><br />
    <%= f.text_field :website %>
  </div>
<% end %>

user_steps/_form.html.erb

<%= form_for @user, url: wizard_path do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>
  <%= yield f %>
  
  <div class="actions">
    <%= f.submit "Continue" %>
    or <%= link_to "skip this step", next_wizard_path %>
  </div>
<% end %>

users/new.html.erb

<h1>Sign Up</h1>

<%= form_for @user do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation %>
  </div>

  <div class="actions">
    <%= f.submit "Sign Up" %>
  </div>
<% end %>

users_controller.rb

def create
  @user = User.new(params[:user])
  if @user.save
    session[:user_id] = @user.id
    redirect_to user_steps_path
  else
    render :new
  end
end

models/user.rb

validates_format_of :twitter_username, without: /\W/, allow_blank: true 
# validates_presence_of :twitter_username, if: :on_social_step?

Screencast: Queue Classic

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

config/application.rb

# Although not shown in the episode, it is a good idea to uncomment this line in config/application.rb
# This is because the schema we are generating here cannot be represented in Ruby.
config.active_record.schema_format = :sql

terminal

rails g migration add_queue_classic
rails g migration setup_queue_classic
rake db:migrate
rake qc:work
rails c

rails console

QC.enqueue "puts", "hello world"
QC.enqueue "puts", msg: "hello world"
QC.enqueue "puts", "msg" => "hello world"

Gemfile

gem 'queue_classic', '2.0.0rc12'

lib/tasks/queue_classic.rake

require "queue_classic"
require "queue_classic/tasks"

config/initializers/queue_classic.rb

ENV["DATABASE_URL"] = "postgres://localhost/mailer_development"

migrations/*add_queue_classic.rb

def up
  create_table :queue_classic_jobs do |t|
    t.string :q_name
    t.string :method
    t.text :args
    t.timestamp :locked_at
  end
  add_index :queue_classic_jobs, :id
end

def down
  drop_table :queue_classic_jobs
end

migrations/*setup_queue_classic.rb

def up
  QC::Queries.load_functions
end

def down
  QC::Queries.drop_functions
end

newsletters_controller.rb

QC.enqueue "Newsletter.deliver", params[:id]

models/newsletter.rb

def self.deliver(id)
  newsletter = find(id)
  # raise "Oops"
  sleep 10 # simulate long newsletter delivery
  newsletter.update_attribute(:delivered_at, Time.zone.now)
end

Screencast: Migration nach PostgreSQL

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

terminal

psql --version
brew install postgresql
initdb /usr/local/var/postgres
pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start
which psql
rails new blog -d postgresql
rake db:create:all
rails g scaffold article name content:text
rake db:migrate
psql blog_development
rails db
gem install taps
taps
taps server sqlite://db/development.sqlite3 rbates secret
taps pull postgres://rbates@localhost/store_development http://rbates:secret@localhost:5000

rails db

select * from articles;
\d
\d articles
\?
\h
\h select
\q

config/database.yml

development:
  adapter: postgresql
  encoding: unicode
  database: blog_development
  pool: 5
  username: rbates
  password:

test:
  adapter: postgresql
  encoding: unicode
  database: blog_test
  pool: 5
  username: rbates
  password:

Gemfile

gem "pg"

Screencast: DataTables

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

Gemfile

group :assets do
  gem 'jquery-datatables-rails', github: 'rweng/jquery-datatables-rails'
  gem 'jquery-ui-rails'
end

gem 'will_paginate'

app/assets/javascripts/application.js

//= require dataTables/jquery.dataTables

app/assets/stylesheets/application.css

/*
 *= require jquery.ui.core
 *= require jquery.ui.theme
 *= require dataTables/src/demo_table_jui
*/

app/assets/javascripts/products.js.coffee

jQuery ->
  $('#products').dataTable
    sPaginationType: "full_numbers"
    bJQueryUI: true
    bProcessing: true
    bServerSide: true
    sAjaxSource: $('#products').data('source')

views/products/index.html.erb

<table id="products" class="display" data-source="<%= products_url(format: "json") %>">
  <thead>
    <tr>
      <th>Product Name</th>
      <th>Category</th>
      <th>Release Date</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

products_controller.rb

def index
  respond_to do |format|
    format.html
    format.json { render json: ProductsDatatable.new(view_context) }
  end
end

app/datatables/products_datatable.rb

class ProductsDatatable
  delegate :params, :h, :link_to, :number_to_currency, to: :@view

  def initialize(view)
    @view = view
  end

  def as_json(options = {})
    {
      sEcho: params[:sEcho].to_i,
      iTotalRecords: Product.count,
      iTotalDisplayRecords: products.total_entries,
      aaData: data
    }
  end

private

  def data
    products.map do |product|
      [
        link_to(product.name, product),
        h(product.category),
        h(product.released_on.strftime("%B %e, %Y")),
        number_to_currency(product.price)
      ]
    end
  end

  def products
    @products ||= fetch_products
  end

  def fetch_products
    products = Product.order("#{sort_column} #{sort_direction}")
    products = products.page(page).per_page(per_page)
    if params[:sSearch].present?
      products = products.where("name like :search or category like :search", search: "%#{params[:sSearch]}%")
    end
    products
  end

  def page
    params[:iDisplayStart].to_i/per_page + 1
  end

  def per_page
    params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
  end

  def sort_column
    columns = %w[name category released_on price]
    columns[params[:iSortCol_0].to_i]
  end

  def sort_direction
    params[:sSortDir_0] == "desc" ? "desc" : "asc"
  end
end

Screencast: Globalize3

 

Downloads in verschiedenen Formaten:

mp4
m4v
webm
ogg

 

Resourcen:

Gemfile

gem 'globalize3'

terminal

rails g migration create_article_translations
rake db:migrate
rails c

rails console

I18n.locale
Article.first.name
I18n.locale = :wk
Article.first.update_attribute(:name, "Ahhyya")
I18n.locale = :en

db/migrate/create_article_translations.rb

class CreateArticleTranslations < ActiveRecord::Migration
  def up
    Article.create_translation_table!({
      name: :string,
      content: :text
    }, {
      migrate_data: true
    })
  end

  def down
    Article.drop_translation_table! migrate_data: true
  end
end

models/article.rb

translates :name, :content

config/application.rb

config.i18n.fallbacks = true