Websiteentwicklung: Ruby on Rails/ Druckversion

Druckversion des Buches Websiteentwicklung: Ruby on Rails
  • Dieses Buch umfasst derzeit etwa 19 DIN-A4-Seiten einschließlich Bilder (Stand: 1.September 2009).
  • Wenn Sie dieses Buch drucken oder die Druckvorschau Ihres Browsers verwenden, ist diese Notiz nicht sichtbar.
  • Zum Drucken klicken Sie in der linken Menüleiste im Abschnitt „Drucken/exportieren“ auf Als PDF herunterladen.
  • Mehr Informationen über Druckversionen siehe Hilfe:Fertigstellen/ PDF-Versionen.
  • Hinweise:
    • Für einen reinen Text-Ausdruck kann man die Bilder-Darstellung im Browser deaktivieren:
      • Internet-Explorer: Extras > Internetoptionen > Erweitert > Bilder anzeigen (Häkchen entfernen und mit OK bestätigen)
      • Mozilla Firefox: Extras > Einstellungen > Inhalt > Grafiken laden (Häkchen entfernen und mit OK bestätigen)
      • Opera: Ansicht > Bilder > Keine Bilder
    • Texte, die in Klappboxen stehen, werden nicht immer ausgedruckt (abhängig von der Definition). Auf jeden Fall müssen sie ausgeklappt sein, wenn sie gedruckt werden sollen.
    • Die Funktion „Als PDF herunterladen“ kann zu Darstellungsfehlern führen.


('Dies ist die Druckversion, mit allen Inhalten auf einer Webseite. Für das Lesen Online ist die Version in einzelnen Seiten besser geeignet')


Lieber Leser, liebe Leserin,

Das Logo von Rails

Ruby on Rails (RoR) ist ein mächtiges Framework für die Entwicklung von Webapplikationen. Dieses Buch bietet einen schnellen Einblick in RoR - wenn man sich mit HTML, CSS, Javascript und einer serverseitigen Sprache wie z.B. PHP schon auskennt. Diese Version des Buches beschreibt Rails 3.2.6

Die Beispiel-Applikation, die im Laufe des Buches entwickelt wird, ist auch online:

  • gesamter code als zip-Datei zum Download - achtung veraltet!
  • hallo welt applikation auf heroku

Es gibt noch ein wesentlich umfangreicheres Wikibook Ruby on Rails für die Vertiefung.

Viel Spaß beim Lernen und Ausprobieren!



Was ist Ruby on Rails

"Ruby on Rails" besteht aus zwei Teilen: Ruby ist eine Open Source Skriptsprache, ähnlich wie Perl oder Python. Ruby kann in verschiedenen Bereichen verwendet werden. Rails ist ein umfangreiches Web-Framework für diese Sprache.


 
Das Logo von Ruby

Was zeichnet Ruby aus?

Bearbeiten
  • dynamische Sprache
  • hoch-dynamische Community
 
Das Logo von Rails

Was zeichnet Rails aus?

Bearbeiten

DRY - Don't Repeat Yourself

Bearbeiten

Rails versucht Code-Verdopplungen zu vermeiden. Man schreibt immer sehr kurze Codes, und man kann viel damit erreichen.

Convention over Configuration

Bearbeiten

Rails hat viele, viele Konventionen. z.B. wo die Dateien gespeichert werden, wie die Datenbanken heißen, u.s.w.

Wenn man sich an die Konventionen hält ist das Leben einfach, man hat wenig Arbeit, und alles funktioniert. (Wenn man sich nicht an die Konventionen halten will, kann man sich das Leben beliebig schwierig machen.)


Model - View - Controller

Bearbeiten

Die Idee von Model - View - Controller wird in vielen Web-Applikationen verwendet. Es handelt sich um einen Pattern, der schon lange Zeit für die Erstellung von Benutzer-Interfaces verwendet wird.

Was MVC für ein eine Web-Applikation bedeutet, sei an einem konkreten Beispiel erklärt: Wir wollen ein Kursverwaltungssystem programmieren, in dem Leute Kurse buchen können.

 

Das Model
ist ein Modell unseres Problemfelds. Für die Kursverwaltung brauchen wir also Kurse, User, Buchungen, etc. Diese sind in einer Datenbank gespeichert, in Tabellen namens kurse, user, buchungen. Für die Programmierung werden diese Datenbank-Tabellen auf Klassen umgelegt: in der Kurs-Klasse gibt es mehrere Kurs-Objekte. Dies erfolgt automatisch; wir brauchen - im Idealfall - keine einzige Zeile SQL zu schreiben.
Die View
behandelt alle Probleme der Darstellung. Die View ist also eine Datei die hauptsächlich HTML, eventuell CSS und Javascript. Ein bisschen Ruby-Code sorgt für die Details der Anzeige, z.B. eine Schleife über alle User oder eine bedingte Anzeige: nur zukünftige Kurse.
Der Controller
nimmt die Daten aus der URL und/oder die Parameter entgegen, ruft das richtige Modell auf, bereitet die richtigen Daten für die View vor... er hält also alles zusammen. Achtung: der Controller kann weder HTML noch SQL enthalten - das ist nicht seine Aufgabe!

Rails unterstützt Dich beim Schreiben von automatisierten Tests. Damit kann man sehr gut Test Driven Development (TDD) machen: erst den Test schreiben, dann erst das Programm dazu.

Wer verwendet Ruby on Rails?

Bearbeiten

Scribd und Slideshare sind wohl die berühmtesten Rails-Applikationen. In Europa verwendet XING (in Hamburg) Rails.

Warum sollte ich 'nicht' RoR verwenden?

Bearbeiten

Für sehr kleine Web-Applikationen ist der Einsatz eines großen Frameworks wie Rails nicht sinnvoll.




Die Sprache Ruby

Ruby ist eine Skriptsprache.

Der Interpreter

Bearbeiten

Du kannst den Ruby-Interpreter direkt aufrufen und Befehle testen:

    D:\>irb
    irb(main):001:0> 1+1
    => 2
    irb(main):002:0> exit

Ein einfaches Skript

Bearbeiten

Ruby-Programme speichert man in Dateien mit der Endung .rb Hier ein einfaches Programm, nenne die Datei hallo.rb

 puts "Hallo Welt"

Nun kannst Du das Programm auf der Kommendozeile starten:

 d:/>ruby hallo.rb
 hallo Welt

Datentypen, Kontrollstrukturen, Klassen

Bearbeiten

Wie in Skriptsprachen üblich verwendet man ein und dieselbe Variable für verschiedene Datentypen.

Im Unterschied zu anderen Skriptsprachen führt Ruby kaum automatische Typ-Konvertierung durch.

....

Ein paar einfache Beispiele:

https://gist.github.com/3462049




Eine Ruby on Rails Applikationen von Außen

Woran erkennt man eine Rails-Applikation?

Betrachten wir dazu die Demo-Site von Redmine, einer Projektmanagement-Applikation die in Ruby geschrieben ist.

http://demo.redmine.org/

Die URLs einer Rails-Applikation haben meist keine Endung, oft keine Parameter. Statt dessen ist die Information direkt im Pfad:

 http://demo.redmine.org/projects/learnrails/boards
 http://demo.redmine.org/projects/learnrails/boards/453
 http://demo.redmine.org/projects/learnrails/boards/454
 http://demo.redmine.org/projects/learnrails/boards/455


Dahinter stecken aber nicht Ordner in einem Filesystem, sondern diese URL wird auf Controller, Actions und Parameter umgeschrieben. der Kommandozeilen-Befehl rake routes zeigt die Regeln dafür an:

 GET  /projects/:project_id/boards      {:controller=>"boards", :action=>"index" }
 GET  /projects/:project_id/boards/:id  {:controller=>"boards", :action=>"show"  }

Die action index zeigt dabei eine Liste (der Boards) an, die action show zeigt ein einzelnes Board an.

Für fortgeschrittene ProgrammiererInnen

Bearbeiten

Diese Struktur der URLs ist aber nur der offensichtlichste Teil eines tiefergehenden Plans. Nicht nur GET, sondern auch die anderen HTTP-Methoden werden verwendet:


 GET    /dings           {:controller=>"dings", :action=>"index"  }   # zeigt eine liste aller dinge
 POST   /dings           {:controller=>"dings", :action=>"create" }   # erzeugt ein neues ding (id steht noch nicht fest)
 GET    /dings/new       {:controller=>"dings", :action=>"new"    }   # zeigt ein Eingabeformular für ein ding 
 GET    /dings/:id/edit  {:controller=>"dings", :action=>"edit"   }   # zeigt ein Bearbeitungsformular für ein bestehendes ding
 GET    /dings/:id       {:controller=>"dings", :action=>"show"   }   # zeigt ein bestimmtes ding an
 PUT    /dings/:id       {:controller=>"dings", :action=>"update" }   # speichert/ändert ein bestehendes ding 
 DELETE /dings/:id       {:controller=>"dings", :action=>"destroy"}   # löscht ein ding

Das ist die Grundstruktur die für verschiedene Ressourcen immer wieder gleiche verwendet werden kann. Die Idee dahinter ist REST[1].

Webservice

Bearbeiten

Mit der Endung .xml kann die Rails-Applikation auch XML als Output liefern:

 GET    /dings.xml
 POST   /dings.xml
 GET    /dings/new.xml
 GET    /dings/:id/edit.xml
 GET    /dings/:id.xml
 PUT    /dings/:id.xml
 DELETE /dings/:id.xml

Damit bietet die Web-Applikation automatisch eine REST Webservice Schnittstelle an. Das kann man für AJAX verwenden oder als API für die Applikation.

(Oder man ignoriert es erst mal.)





Erste Rails App

Die Rails Applikation erzeugen

Bearbeiten

Bei der Arbeit mit Ruby on Rails spielt nicht nur der eigentliche Source-Code, sondern auch die Kommandozeile eine wichtige Rolle. Schon der erste Schritt passiert auf der Kommandozeile: um die Grund-Struktur der Rails-Applikation zu erzeugen wird der rails-Befehl aufgerufen:

 rails new hallowelt 
 cd hallowelt

Damit wird eine Reihe von Ordnern und Dateien erzeugt:

 

Die Kommandozeile wird auch weiterhin (mehrfach) gebraucht. (Ab jetzt sind alle Befehle im Hauptordner der App abzusetzen.)

Das Programm bundle installiert die richtigen Gems

 bundle install

Das Programm rake führt Befehle aus, die im Rakefile definiert sind. Bevor wir starten können, müssen wir die Datenbank mit folgendem Befehl erzeugen:

 rake db:migrate

Nun können wir den Webserver auf der Kommandozeile starten:

 rails server


Nun kann ich mir die App am localhost auf port 3000 im Browser ansehen:

 http://localhost:3000/

Scaffolding

Bearbeiten

Nach dem Model-View-Controller Prinzip müssen wir für unsere Datenbank der Hallo-Welt Meldungen

  1. eine Tabelle in der Datenbank anlegen
  2. ein Model erzeugen
  3. einen Controller (mit eventuell verschiedenen Actions) erzeugen
  4. für die Actions jeweils Views erzeugen

Rails hilft uns, das Alles auf einmal zu machen.

Scaffold erzeugen

Bearbeiten

Dazu verwenden wir das generate-script und erzeugen damit ein scaffold - ein Gerüst:

 rails generate scaffold Hallo von:string meldung:text farbe:string

Das erste Argument hallo ist der Name des Modells, danach folgen die Eigenschaften und Datentypen des Modells.

In diesem Fall will ich zu jedem "hallo" abspeichern: von dem die Meldung ist, den Text der Meldung und welche Farbe die Meldung haben soll.

Nun müssen wir noch die Datenbank-Tabelle wirklich anlegen, das geschieht mit

 rake db:migrate

Die Datenbank

Bearbeiten

Mit dem Befehl

 rails dbconsole

startet man eine shell für die Datenbank. Hier kann man z.B. mit

 .schema hallos

die gerade angelegte Tabelle betrachten:

 CREATE TABLE "hallos" (
     "id"         INTEGER         PRIMARY KEY AUTOINCREMENT NOT NULL, 
     "von"        varchar(255), 
     "meldung"    text, 
     "farbe"      varchar(255), 
     "created_at" datetime NOT NULL, 
     "updated_at" datetime NOT NULL
 );

Als Name für das Modell hatten wir Hallo gewählt, rails hat automatisch die Mehrzahl für die Bezeichnung der Tabelle gewählt.

Ein Primärschlüssel namens id und die beiden Felder created_at und updated_at wurden ebenfalls automatisch angelegt.

Die Datentypen wurden auch von Ruby auf Datenbank übersetzt: aus string wurde varchar(255).

Das Modell in app/models/hallo.rb

Bearbeiten

Die Klassen-Datei für das Modell wurde angelegt, ist aber noch leer.

Hier könnten wir Methoden eintragen die die innere Logik des Modells abbilden.

Für die Validierung der Daten bietet Rails viele Praktische Kurz-Schreibweisen an. Wenn wir z.B. sicherstellen wollen, dass als Farbe nur ein HTML-Farbcode wie #FF0033 gespeichert werden kann, dann reicht dazu eine Zeile im Modell:

 validates :farbe, :format => { :with => /^#......$/,  :message => "nur Farb-Code mit # und 6 Stellen (hexadezimal) erlaubt" }
  Dazu gibt es einen Rails Guide [2] Validations

Das erste Argument ist der Name der Spalte / der Eigenschaft, mit dem benannten Argument :with gibt man eine Regular Experssion zur Überprüfung an, und mit :message eine passende Fehlermeldung.

Der Controller in app/controllers/hallos_controller.rb

Bearbeiten

Diese Datei ist umfangreich: sie enthält 7 vorgefertigte Actions:

  • index - listet alle Meldungen auf
  • show - zeigt eine bestimmte meldung an (ID notwendig!)
  • new - zeigt ein Eingabeformular für eine neue Meldung an (weiter bei create)
  • edit - zeigt eine Meldung in einem Bearbeitungsformular an (ID notwendig!, weiter bei update)
  • create - erzeugt eine neue Meldung
  • update - verändert eine Meldung
  • destroy - löscht eine Meldung (ID notwendig!)

Betrachten wir die einfachste Action: show zeigt eine bestimmte Meldung an. Beim Aufruf dieser Action muss eine id als Parameter übergeben werden:

 def show
   @hallo = Hallo.find(params[:id])
   respond_to do |format|
     format.html # show.html.erb
     format.json { render json: @hallo }
   end
 end


Über den Parameter id wird der Datensatz Hallo als Objekt geladen und in der Instanzvariable @hallo des Controllers gespeichert.

Die Action funktioniert aber nicht nur für den Fall, dass Sie HTML als Output liefern soll. Auch JSON als Output ist vorgesehen. Dafür sind 4 weiter Zeile Code notwendig:

 def show
   @hallo = Hallo.find(params[:id])
   respond_to do |format|
     format.html # show.html.erb
     format.json { render json: @hallo }
   end
 end

Routing - von der URL zur Action

Bearbeiten

Mit dem Kommandozeilen-Befehl rake routes kann man sehen wie URLs, Controller, Actions zusammen hängen. Dies wird in der Datei config/routes.rb festgelegt, wurde aber durch das Scaffolding automatisch erledigt. Hier der Output von rake routes:

       hallos GET    /hallos(.:format)                  {:action=>"index", :controller=>"hallos"}
              POST   /hallos(.:format)                  {:action=>"create", :controller=>"hallos"}
    new_hallo GET    /hallos/new(.:format)              {:action=>"new", :controller=>"hallos"}
   edit_hallo GET    /hallos/:id/edit(.:format)         {:action=>"edit", :controller=>"hallos"}
        hallo GET    /hallos/:id(.:format)              {:action=>"show", :controller=>"hallos"}
              PUT    /hallos/:id(.:format)              {:action=>"update", :controller=>"hallos"}
              DELETE /hallos/:id(.:format)              {:action=>"destroy", :controller=>"hallos"}

Ganz links ist für manche Routes ein Name angegeben, den man innerhalb von Views und Controllern verwenden kann. In der zweiten Spalte finden Sie die HTTP Methode, anschießend die URL und zuletzt die Kombination von Controller und Action die hier aufgerufen werden.

Die erste Route hallos ruft die action index auf und alle Hallo-Meldungen werden angezeigt.

Die route mit dem namen hallo (3.von unten) bedeutet also: Wenn mit GET ein Aufruf von http://localhost:3000/hallos/14 erfolgt, dann wird der Controller hallos mit Action show aufgerufen. Wo :id steht kann also eine Zahl eingesetzt werden. Wo :format steht kann json eingefügt werden: Wenn mit GET ein Auftruf von http://localhost:3000/hallos/14.json erfolgt, dann wird der Controller hallos mit Action show aufgerufen, er soll dann JSON als Output produzieren.

Nun wäre ein guter Zeitpunkt die Applikation auszuprobieren und ein paar Meldungen einzufügen, wieder zu verändern oder zu löschen

Die Views in app/views/hallos/*.html.erb

Bearbeiten

Vier Views wurden automatisch angelegt:

  • index.html.erb - listet alle Meldungen auf
  • show.html.erb - zeigt eine bestimmte meldung an (ID notwendig!)
  • new.html.erb - zeigt ein Eingabeformular für eine neue Meldung an (weiter bei create)
  • edit.html.erb - zeigt eine Meldung in einem Bearbeitungsformular an (ID notwendig!, weiter bei update)

Das Format der Views ist Embedded Ruby (Endung .erb). Eine View besteht hauptsächlich aus HTML, mit <% ... %> kann Ruby eingebettet werden das ausgeführt wird. Mit <%= ... %> kann Ruby eingebettet werden das Output liefert. Dabei werden hauptächlich die Instanz-Variablen des Controllers verwendet.


Die View show.html.erb zeigt eine Meldung an.

   <p id="notice"><%= notice %></p>

    <p>
      <b>Von:</b>
      <%=h @hallo.von %>
    </p>
    <p>
      <b>Meldung:</b>
      <%=h @hallo.meldung %>
    </p>
    <p>
      <b>Farbe:</b>
      <%=h @hallo.farbe %>
    </p>
    <%= link_to 'Edit', edit_hallo_path(@hallo) %> |
    <%= link_to 'Back', hallos_path %>

Die beiden letzten Zeilen erzeugen zwei Links. Das erste Argument von link_to ist der Link-Text. Das zweite Argument ist die URL. Hier werden Methoden aufgerufen, die automatisch für alle benannten routes vorhanden sind (siehe rake routes).

Die route hallo braucht keine Parameter, mit hallo_path erhält man die URL ohne Hostnamen, mit hallo_url die vollständige URL.

Bei edit_hallo ist es etwas komplizierter: diese Route muss wissen, welches Objekt gemeint ist. Hier kann man als Argument entweder das ganze Objekt oder nur die id des Objekts übergeben. Auch hier gibt es wieder die Formen edit_hallo_path und edit_hallo_url.

Die Darstellung der Meldung können wir ganz verändern: Die Farbe wird wirklich als Farbe verwendet und nicht als Text angezeigt

   <div class="meldung">
        <p><%=h @hallo.von %> sagt:</p>
        <blockquote style="color: <%= @hallo.farbe %>"><%=h @hallo.meldung %></blockquote>
    </div>
    <%= link_to 'Edit', edit_hallo_path(@hallo) %> |
    <%= link_to 'Back', hallos_path %>


Verändern Sie auch index.html.erb so, dass die Farbe wirklich als Farbe verwendet wird! Achtung: in der index-View gibt es zwei Variablen: @hallos ist ein Array aller Meldungen, hallo ist die jeweils aktuelle Meldung.

Eine neue Homepage einsetzen

Bearbeiten

Der hallo-Controller wäre ein sehr gut Homepage für unsere 'Hallo Welt' Applikation. Um das zu erreichen, muss man die Datei public/index.html löschen und in config/routes.rb folgende Zeile einfügen:

 root :to => "hallos#index"

Damit ist die erste Webapplikation in Ruby on Rails fertig.



  1. Tilkov(2007): A Brief Introduction to REST. In: InfoQ.
  2. Rails Guide Validations

Konfiguation und Datenbank

Wenn man ein Projekt mit

  rails new projektname

startet, speichert Rails die Daten in einer sqlite-Datenbank. Das ist besonders einfach: alle Daten sind in der Datei db/development.sqlite3 zu finden.

Wenn man das Projekt gleich mit

  rails new -d mysql projektname

startet wird statt dessen mysql verwendet. Nun wollen wir uns näher mit der Konfiguration befassen und dabei lernen wie man ein laufendes Projekt auf ein mysql-Datenbank umstellt.

Konfigurations-Dateien

Bearbeiten

Im Ordner config sind u.a. folgende Dateien zu finden:

database.yml
Konfiguration der Datenbanken für die verschiedenen Server
routes.rb
Konfiguation der URLs und ihrer Umrechnung auf Controller und Actions
environment.rb
weitere Konfiguration
locales/en.yml
Texte für die englische Version der Website (hier könnte man de.yml, it.yml, u.s.w. einfügen)

Datenbank

Bearbeiten

Die Datei database.yml enthält normalerweise folgenden Code:

   development:
     adapter: sqlite3
     database: db/development.sqlite3
     pool: 5
     timeout: 5000
   #
   test:
     adapter: sqlite3
     database: db/test.sqlite3
     pool: 5
     timeout: 5000
   #
   production:
     adapter: sqlite3
     database: db/production.sqlite3
     pool: 5
     timeout: 5000


Hier sehen wir dass in Rails von Anfang an drei verschiedene Server vorgesehen sind:

development
ist die Datenbank für die Entwicklung, wenn ich das Programm schreibe und ausprobiere
test
ist die Datenbank für die automatischen Tests. Diese Datenbank wird auch automatisch erzeugt und wieder gelöscht
production
ist die Datenbank für den Echt-Betrieb


Wenn ich mysql statt sqlite verwenden will muss ich einerseits entsprechende Datenbanken in meinem MySQL Server anlegen und andererseits die Konfigurationsdatei ändern. Hier ein Beispiel für das Projekt 'chor': (Achtung beim Kopieren dieses Codes: diese Konfigurationsdatei verträgt keine Tabulatoren und keine Leerzeichen vor development, test, production!)

   development:
     adapter: mysql
     encoding: utf8
     reconnect: false
     database: chor_development
     pool: 5
     username: root
     password:
     host: localhost
   #
   test:
     adapter: mysql
     encoding: utf8
     reconnect: false
     database: chor_test
     pool: 5
     username: root
     password:
     host: localhost
   #
   production:
     adapter: mysql
     encoding: utf8
     reconnect: false
     database: chor_production
     pool: 5
     username: root
     password: 
     host: localhost


Die Konvention, an den Applikationsnamen mit Unterstrich etwas anzuhängen um den Datenbanknamen zu erzeugen hat eine angenehme Nebenwirkung: in phpmyadmin werden die Datenbanken gruppiert.


Datenbank und Model

Wie arbeiten Datenbank und Model zusammen?

Datenbank verändern mit Migrations

Bearbeiten

Die Problemstellung

Bearbeiten

Bei Web-Applikationen tritt folgendes Problem auf: im Laufe der Entwicklung ändert sich das Datenbank-Schema. Gleichzeitig ist die Applikation aber schon im Echt-Betrieb, man kann die Produktions-Datenbank nicht einfach löschen und von Grund auf neu bauen.

Es ist also notwendig, die Veränderungs-Schritte der Datenbank zu speichern, und auf allen Servern (verschiedene Entwicklungs-Rechner, Test, Produktion) in der gleichen Reihenfolge nachzuvollziehen.

Ruby on Rails bietet dafür eine Lösung: Migrations.

Eine Migration ist dabei ein Schritt der Veränderung - wobei dieser Schritt immer in beide Richtungen definiert ist.

Im Ordner /db/migrations/ sind die einzelnen Migrations gespeichert.

Nach der Erzeugung des scaffolds für hallo sieht das z.B. so aus:

 20090829142637_create_hallos.rb

Diese Datei enthält das Anlegen der Tabelle für das Model Hallo:

class CreateHallos < ActiveRecord::Migration
    def self.up
      create_table :hallos do |t|
	t.string :von
	t.text :meldung
	t.string :farbe
	t.timestamps
      end
    end
    def self.down
      drop_table :hallos
    end
  end


Eine Migration erstellen

Bearbeiten

Mit dem Kommandozeilen-Befehl

 ↯rake db:migration↯
 rake db:migrate
 (siehe auch rake -T [korrigiert uwe.schimon@mailpost.de])

werden jeweils alle neuen Migrations ausgeführt. Mit

 rake db:rollback

kann der letzte Schritt rückgängig gemacht werden. Welcher Migrations-Schritt bereits angewandt wurde wird in der Datenbank salbst gespeichert, in der Tabelle schema_migrations

 

Wenn man die Datenbank verändern will - zum Beispiel um eine neue Spalte in die Tabelle einzufügen - so kann man selbst eine migration erzeugen:

 rails generate migration AddWiederholungToHallo wiederholung:integer

Aus der Namens-Konvention erzeugt der Generator gleich einen guten Entwurf für die Migration:


 class AddWiederholungToHallo < ActiveRecord::Migration
   def self.up
     add_column :hallos, :wiederholung, :integer
   end
   def self.down
     remove_column :hallos, :wiederholung
   end
 end

Was noch zu tun ist

Bearbeiten
  Dazu gibt es einen Rails Guide [1] Migrations
  Siehe dazu die Rails API[2] ActiveRecord Migration


Nach dem Hinzufügen oder Löschen von Spalten aus der Tabelle muss das Model nicht verändert werden! Die Spalten werden automatisch als Attribute des Objekts übernommen. Der Controller muss meist auch nicht verändert werden.

Nur die Views (Eingabemasken, Anzeige des Objekts) müssen verändert werden!

Daten lesen und speichern mit dem Model

Bearbeiten

Wir arbeitet das Model mit der Datenbank zusammen? Das werden wir nun interaktiv erforschen


Daten Suchen und Finden

Bearbeiten

Es gibt eine interaktive Shell für Rails. Gestartet wird sie mit

 rails console

Nun kann man ruby-befehle eintippen und sieht sofort das Ergebnis.

   >> Hallo.find(1)
   => #<Hallo id: 1, von: "Brigitte", meldung: "Hallo Welt, es freut mich sehr Dich zu sehen!", farbe: "#FF0000", created_at: "2009-08-29 14:28:54", updated_at: "2009-08-29 14:28:54">

Die Klasse Hallo hat (wie jedes Klasse des Models) eine Methode find. Mit find(1) suche ich nach dem Objekt das in der Datenbank die id=1 hat.

Mit find(:all) suche ich nach allen Objekten. Das gibt viel output (hier gekürzt!)

   >> Hallo.find(:all)
   => [#<Hallo id: 1, von: "Brigitte">, #<Hallo id: 2, von: "Karin">, #<Hallo id: 3, von: "Hilmar">]
  

Daten anlegen

Bearbeiten

Hier kann man auch neue Objekte anlegen:

   >> h = Hallo.new
   => #<Hallo id: nil, von: nil, meldung: nil, farbe: nil, created_at: nil, updated_at: nil>
   >> h.von = "Tippse"
   => "Tippse"
   >> h.meldung = "Auch ohne das Web kann ich einfügen"
   => "Auch ohne das Web kann ich einf\201gen"
   >> h
   => #<Hallo id: nil, von: "Tippse", meldung: "Auch ohne das Web kann ich einf\201gen", farbe: nil, created_at: nil, updated_at: nil>

das Objekt existiert derzeit nur im Hauptspeicher, im laufenden Programm - es ist noch nicht in der Datenbank gespeichert. (Deswegen hat es auch noch keine id). mit save kann ich versuchen das Objekt zu speichern:

   >> h.save
   => false

Der Rückgabewert false bedeutet: das Speichern hat nicht funktioniert. Warum? Weil die Farbe nicht gespeichert werden kann, ich habe ja eine Validierung auf dem Farb-Feld Programmiert:

  # im Model 
  validates_format_of :farbe, :with => /^#......$/,  :message => "nur Farb-Code mit # und 6 Stellen (hexadezimal) erlaubt" 

Also muss ich die Farbe korrekt setzen und kann dann speichern:

   >> h.farbe="#00FF00"
   => "#00FF00"
   >> h.save
   => true

Danach hat h auch eine id, und werte in created_at und updated_at:

   >> h
   => #<Hallo id: 7, von: "Tippse", meldung: "Auch ohne das Web kann ich einf\201gen", farbe: "#00FF00", created_at: "2009-08-31 11:46:06", updated_at: "2009-08-31 11:46:06">



Controller und View

  1. Rails Guide Migrations
  2. Rails API ActiveRecord Migration

Views, Partials und Layouts

  Dazu gibt es einen Rails Guide [1] Layouts and Rendering

Beim Schreiben der View ist Dir vielleicht aufgefallen, dass nirgendwo ein head oder body zu lesen ist. Diese Teile der Webseite, die für die einzelnen Seiten der Applikation gleich sind, werden nicht in der View selbst gespeichert:

 



Für eine einfache Webapplikation brauchen wir nur ein Layout: es heisst application.html.erb.

Die vom Scaffold erstellte Datei enthält HTML5, und an vier Stellen Ruby:

  • ein Link zum (gesammelten) Stylesheet
  • ein Link zum (gesammelten) Javascript
  • ein Tag der Code einfügt, der Cross Site Request Forgeries verhindert
  • der Befehl yield, hier wird die View eingefügt

Eine Veränderung im Layout kann beim Verständnis der Applikation helfen: Im Titel geben wir automatisch den Controller und die Action aus:

 <title><%= controller.controller_name %> : <%= controller.action_name %></title>
 

Partials

Bearbeiten

Einfaches Partial

Bearbeiten

In den Views edit.html.erb und new.html.erb wird genau das selbe Formular verwendet. Dieses Formular ist deswegen nur einmal gespeichert, in einem Partial. In beiden Views steht nur

  <%= render  "form" %>

Achtung: damit wird die Datei _form.html.erb geladen! (Beachte den Unterstrich!)

Das Partial selbst enthält einfach den Code mit dem Formular.

Partial mit Schleife

Bearbeiten

Die Darstellung der hallo-Meldungen in show und index können wir auch vereinheitlichen. Wir erzeugen ein Partial _meldung.html.erb', das eine einzelne Meldung darstellt. Diese Meldung ist in der Varialbe meldung zu finden - das ergibt sich aus dem Dateinamen des Partials:

 <div class="meldung">
     <p><%=h meldung.von %> sagt:</p>
     <blockquote><%=h meldung.meldung %></blockquote>
  </div>

Dieses Partial kann nun in der View von show einfach aufgerufen werden:

 <%= render :partial => "meldung", :object => @hallo %>

In der View von index war bisher eine Schleife über das Array @hallos nötig. Dies entfällt mit dem Aufruf des Partials mit dem Argument :collection:

 <%= render :partial => "meldung", :collection => @hallos %>
Bearbeiten
  Siehe dazu die Rails API[2] AssetTagHelper

Wir haben schon verschiedene Methoden kennen gelernt, wie wir vermeiden können fixe URLs zur programmieren:


   <%= link_to 'Edit', edit_hallo_path(@hallo) %> |
   <%= link_to 'Back', hallos_path %>
   <%= stylesheet_link_tag    "application", :media => "all" %>

Analoges gibt es auch noch für Bilder:

   <%= image_tag("rails.png") %>

Für Javascript gibt es auch eine Standard-Einstellung, damit wird normalerweise jquery geladen:

 <%= javascript_include_tag "application" %>


Hinter all diesen Tags steckt fortgeschrittene Magie: die Asset Pipeline.

  Dazu gibt es einen Rails Guide [3] Asset Pipeline



Weitere Quellen


jQuery

Rails unterstützt die Verwendung der Javascript-Library jQuery.

Beispiel

Bearbeiten

Bei der Anzeige der Meldung sollen die Bearbeitungs-Links (show, edit, destroy) unsichtbar sein bis man mit der Maus über die Meldung fährt. Dazu wird der HTML-Code nicht verändert, nur zwei Klassen werden eingeführt: sensor bezeichnet den Bereich der auf die Maus reagieren soll, hide_until_hover muss innerhalb von sensor sein und bezeichnet den Bereich der versteckt wird.


 
Klassen für jquery, siehe demo

Dazu noch der etwas veränderte Code der hallos/index view:

 <div class="meldung sensor">
    <p><%=h hallo.von %> sagt:

     <span class="hide_until_hover">(
       <%= link_to 'Show', hallo_comments_path(hallo) %>
       | 
       <%= link_to 'Edit', edit_hallo_path(hallo) %>
       | 
      <%= link_to 'Destroy', hallo, method: :delete, data: { confirm: 'Are you sure?' } %>
      )</span></p>

    <blockquote><%=h hallo.meldung %></blockquote>
  </div>


Da dieses Javascript nur in den Views von "Hello" gebraucht wird, fügen wir in die Datei app/assets/javascripts/hallos.js ein:

 <script>
    $(document).ready(function() {
      $('.sensor .hide_until_hover')
        .hide();
      $('.sensor')
	.mouseover( function(){
	  $('.hide_until_hover', this).show();
	})
	.mouseout( function(){
	  $('.hide_until_hover', this).hide();
	})
	;
    });
  </script>


Hinweis: es existiert eine leere Datei app/assets/javascripts/hallos.js.coffee. Hier könnte man CoffeeScript einfügen, dass dann in einem separaten Schritt zu Javascript kompiliert wird. Man kann diese Datei einfach löschen, wenn man lieber direkt in Javascript arbeiten will.



Testen

  Dazu gibt es einen Rails Guide [4] Testing

Bei Web-Applikationen, die nicht nur einmal veröffentlicht, sondern auch dauernd weiter entwickelt werden, ist es besonders wichtig automatische Tests zu haben: Wenn ich ein neues Feature einbaue kann ich anschließend die Tests laufen lassen, und somit sicher sein, dass die Applikation noch funktioniert.

Die Test-Umgebung

Bearbeiten

Im Ordner test findet man den Code den man für Tests schreibt. Ein weiterer Teil der Test-Umgebung ist die Datenbank, die in config/database.yml definiert wurde.

Bevor man Tests startet muss man die Test-Datenbank auf den neuesten Stand bringen:

 rake db:migrate
 rake db:test:load

Unit Tests

Bearbeiten

Die Unit-Tests testen die einzelnen Klassen des Models. In unserem sehr einfachen Beispiel-Programm gibt es noch nicht viel zu testen:

In der Datei \test\unit\hallo_test.rb:

   def test_hallo_ohne_farbe_kann_man_nicht_speichern 
       h = Hallo.new 
       assert !h.save 
   end 
   
   
   def test_hallo_mit_farbe_kann_man_speichern 
       h = Hallo.new 
       h.farbe = '#ff0000'
       assert h.save 
   end 

Um diesen Test durchzuführen muss man auf der Kommandozeile in den ordner test wechseln:

 cd test
 ruby unit\hallo_test.rb

Functional Tests

Bearbeiten

Diese Tests prüfen die Funktion der Controller. Durch das scaffolding wurde schon für jede Action ein Test erstellt. Den Test kann man wieder direkt starten:

ruby functional\hallos_controller_test.rb

In dieser Datei ist die Schreibweise etwas anders: jeder Test beginnt mit dem Wort 'test' und einem Namen als String.

test "should show hallo" do
  get :show, :id => hallos(:one).to_param
  assert_response :success
end

hallos(:one) ist ein verweis auf eine "Fixture". Das sind Test-Daten, die vor dem Testlauf aus einer Datei in die Datenbank geladen werden.

So könnte die Datei \test\fixtures\hallo.yml aussehen:

  one:
    von: Brigitte
    meldung: Hallo Welt
    farbe: "#ff0000"

Achtung: dies ist wieder eine Datei im yaml-Format (so wie database.yml), die sehr empfindlich auf einrückungen reagiert!

Damit wird ein Datensatz definiert, der von den Test-Dateien aus als hallos(:one) erreichbar ist.


Integration Tests

Bearbeiten

Alle bisherigen Tests betrachten Details: nur ein Modell, oder nur einen Schritt beim Aufruf über das Web. Für das "große Ganze" gibt es noch Integration Tests. Diese verwendet man zum Beispiel, um zu testen ob das Login funktioniert, und ein User nach dem Login bestimmte Recht hat - also Szenarien mit mehreren Schritten.

Integration Tests werden nicht automatisch erzeugt.


Ausblick: Cucumber

Bearbeiten

Mit Cucumber[5] kann man die intendierte Funktion der Webseite auf "Pseudo-englisch" oder "Pseudo-Deutsch" beschreiben. Damit eignet es sich gut um mit Nicht-Programmierern gemeinsam die Tests zu schreiben.

Wenn alle Tests erfolgreich durchlaufen wird Cucumber grün - deswegen ist es nach der Gurke benannt:


 
Test läuft durch





Assoziationen

  Dazu gibt es einen Rails Guide [6] Active Record Associations

Zwei Modelle sollen eine Beziehung haben, zum Beispiel: zu jeder unserer Hello-Meldungen soll es mehrere Comments geben können, jedes Comment gehört zu genau einem Hallo. Dies nennt man eine 1:n Assoziationen.

Modell und Datenbank

Bearbeiten

Diese Beziehung passiert auf zwei Ebenene: im Modell und in der Datenbank.

In den beiden Modellen wird die Beziehung so aussehen:

 class Comment < ActiveRecord::Base
    belongs_to :hallo
    attr_accessible :text
 end
 class Hallo < ActiveRecord::Base
   has_many :comments, :dependent => :destroy
   attr_accessible :farbe, :meldung, :von
 end

In der Datenbank gibt es einen Fremdschlüssel hallo_id in der comment-Tabelle.

Wenn man das gleich von Anfang an bedenkt kann man diesen Schlüssel schon beim scaffold mit erzeugen lassen:

  rails generate scaffold Comment text:text hallo_id:integer
  rake db:migrate

Unter der URL http://localhost.3000/comments/ kann man jetzt Comments anlegen

Die Objekte sind auf foglende Weise miteinander verbunden: man kann von einem comment immer zu seinem hallo gelangen und umgekehrt:

 @hallo.comments # ---> Array aller Comments
 @comment.hallo  # ---> ein Hallo

Bei einer belongs_to/has_many Beziehung macht es Sinn, die URLs zu verschachteln: Ein Comment ist immer einem Hallo zugeordnet.

       hallo_comments GET    /hallos/:hallo_id/comments(.:format)          comments#index
                      POST   /hallos/:hallo_id/comments(.:format)          comments#create
    new_hallo_comment GET    /hallos/:hallo_id/comments/new(.:format)      comments#new
   edit_hallo_comment GET    /hallos/:hallo_id/comments/:id/edit(.:format) comments#edit
        hallo_comment GET    /hallos/:hallo_id/comments/:id(.:format)      comments#show
                      PUT    /hallos/:hallo_id/comments/:id(.:format)      comments#update
                      DELETE /hallos/:hallo_id/comments/:id(.:format)      comments#destroy
               hallos GET    /hallos(.:format)                             hallos#index
                      POST   /hallos(.:format)                             hallos#create
            new_hallo GET    /hallos/new(.:format)                         hallos#new
           edit_hallo GET    /hallos/:id/edit(.:format)                    hallos#edit
                hallo GET    /hallos/:id(.:format)                         hallos#show
                      PUT    /hallos/:id(.:format)                         hallos#update
                      DELETE /hallos/:id(.:format)                         hallos#destroy
                 root        /                                             hallos#index

Dies erreicht man mit folgender Änderung in config/routes.rb

 resources :hallos do
   resources :comments
 end

Nun können wir die Controller und Views ausprobieren: die URL zum Einstieg lautet http://localhost:3000/hallo/1/comments

Controller

Bearbeiten

Im comments_controller erhalten wir immer den Parameter hallo_id. Wir können also bei jeder einzelnen Action folgende erste Zeile eintragen:

   @hallo = Hallo.find(params[:hallo_id])

Statt das 7mal einzutragen gibt es eine einfachere Möglichkeit: einen before_filter. Die im before_filter angegebene Funktion wird vor jeder Action ausgeführt. Da die Funktion in eine Instanz-Variable schreibt (erkennbar am @ vor dem Variablennamen) ist die Information dann auch in der Action verfügbar.


   class CommentsController < ApplicationController
     before_filter :get_hallo
     ...
     # am Ende der Klasse kommen die privaten Funktionen:
   private
     def get_hallo
        @hallo = Hallo.find(params[:hallo_id])
        if @hallo.nil? then raise "Welches Hallo?" end
     end
   end

Die Methoden create muss auch angepasst werden: das Kommentar wird nicht einfach als Objekt der Klasse Comment erzeugt, sondern wir verwenden die build Methode des Objekts @hello:

   @comment = @hallo.comments.build(params[:comment])


Im index wollen wir nicht alle comments der Datenbank (Comments.all) anzeigen, sondern nur diejenigen, die zum akutellen Hallo gehören:

 def index
   @comments = @hallo.comments
   ...

Neue URLs in Views und Controller

Bearbeiten

In den Views des comment-Controllers kann man jetzt auch @hallo lesen.


In den Views muss man viele Pfade ändern. Ein Beispiel:

aus comment_path() wird hallo_comment_path(@hallo)

Am einfachsten ist es sich die Routes anzeigen zu lassen, um heraus zu finden wie die neue Version der einzelnen Methoden lautet.

Vorher:

    <td><%= link_to 'Show', comment %></td>
    <td><%= link_to 'Edit', edit_comment_path(comment) %></td>
    <td><%= link_to 'Destroy', comment, method: :delete, data: { confirm: 'Are you sure?' } %></td> 

Nachher:

    <td><%= link_to 'Show', hallo_comment_path(@hallo, comment)' %></td>
    <td><%= link_to 'Edit', edit_hallo_comment_path(@hallo,comment) %></td>
    <td><%= link_to 'Destroy',  hallo_comment_path(@hallo,comment), method: :delete, data: { confirm: 'Are you sure?' } %></td> 

Analog für form_for: auch das Formular muss einen längern Pfad benutzen:

   <%= form_for([@hallo,@comment]) do |f| %>

Das Eingabefeld für hallo_id kann man dafür ersatzlos streichen: der Parameter hallo_id wird aus der URL übernommen.


Wenn man nun ein neues Comment speichern will gibt es ein Problem mit dem Controller:

 # POST /comments
 # POST /comments.json
 def create
   @comment = Comment.new(params[:comment])
   respond_to do |format|
     if @comment.save
       format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
       format.json { render json: @comment, status: :created, location: @comment }
     else
       format.html { render action: "new" }
       format.json { render json: @comment.errors, status: :unprocessable_entity }
     end
   end
 end

Das Erzeugen und Abspeichern des Comments funktioniert schon richtig, das Problem tritt danach auf:

       format.html { redirect_to @comment, notice: 'Comment was successfully created.' }

Hier soll zur show-action des neu angelegten Comments umgeleitet werden. Da Comments nun eine verschachtelte Ressource ist, müssen wir die entsprechende URL verwenden:

       format.html { redirect_to hallo_comment_path(@hallo,@comment), notice: 'Comment was successfully created.' }



Vereinfachen

  Dazu gibt es einen Rails Guide [7] Layouts and Rendering in Rails

Die Views, die das Scaffolding für unsere automatisch erzeugt hat, sind nicht miteinander verbunden: Hallos linkt zur zu Hallos-Views, Comments zur zu Comments-Views. Das ist nicht gut zu bedienen, und eigentlich unpraktisch

Nun wollen wir diese Views vereinfachen, und leichter bedinbar machen.

Welche View bleibt?

Bearbeiten

Durch die Vereinfachung werden einige Views wegfallen - aber welche? Betrachent wir folgende routes:

 0              root        /                                                   umgeleitet auf hallos#index
 1            hallos GET    /hallos                             hallos#index    Liste aller Hallos
 2             hallo GET    /hallos/:id                         hallos#show     Anzeigen eines Hallos, ohne Kommentare
 3    hallo_comments GET    /hallos/:hallo_id/comments          comments#index  Liste der Kommentare zu einem Hallo
 4     hallo_comment GET    /hallos/:hallo_id/comments/:id      comments#show   Ein Kommentar


Nummer 2 und Nummer 3 würden sich beide eigenen, um ein Hallo mit allen Kommentaren dazu anzuzeigen. Betrachten wir an die dahinter liegenden Controller: im comments_controller wird @hallo schon gefunden und geladen. D.h. es ist einfach, es auch in der View anzuzeigen. Im hallos_controller werden keine Kommentare geladen. Deswegen ist es einfacher, Nummer 2 nicht weiter zu verwenden, und überall durch Nummer 3 zu ersetzen.

View zeigt Hallo und Kommentare

Bearbeiten

Wir können in die view comments/index.html.erb einfach Code zur Darstellung des Hallos einfügen:

 <h1>Listing comments</h1>
  <p><%= @hallo.von %> sagt:</p>
  <blockquote><%= @hallo.meldung %></blockquote>  

Neu verlinken

Bearbeiten

In der View hallos/index.html.erb können wir nun die Show-Links verändern: statt

 hallo_path( hallo )

auf

 hallo_comments_path( hallo )


User und Login

In diesem Kapitel werden wir eine App um eine n:m Beziehung erweitern: Ausgangspunkt ist tvtogether, eine App die Serien und Folgen von tvrage.com laden kann.

In diesem Kapitel werden wir als ersten Schritt in die App die Möglichkeit einbauen, dass User sich anmelden, einloggen und ausloggen. Dabei unterstützt uns Rails 3 sehr, wir brauchen nur wenig selbst zu Programmieren.

Diese App werden wir um ein Model 'user' erweitern. Zwischen users und episodes besteht eine n:m Beziehung: eine Person kann mehrere Episoden gesehen haben, eine Episode wurde von mehreren Personen gesehen.


User und Authentisierung

Bearbeiten

Wir werden ein Model "User" brauchen, das Namen, E-Mail und das gehashte Passwort gespeichert.

Ausserdem werden wir den Hash session verwenden. Dieser wird von Rails zur Verfügung gestellt. Daten die wir hier speichern sind noch vorhanden, wenn vom gleichen Browser auf die nächste Seite aufgerufen wird. (Das wird mittels Cookies erreicht, diese werden automatisch von Rails gesetzt)

  Dazu gibt es einen Railscast [8] Authentication in Rails 3.1


Diese Funktionalität braucht ein zusätzliches gem: Wir fügen in das Gemfile ein:

 gem 'bcrypt-ruby'

und führen danach einmal

 bundle install

aus. Damit wird das gem installiert. Damit es in der App verwendet wird, müssen wir den Webserver stoppen (mit STRG-C) und neu starten

 rails server


Nun legen wir das User-Model an.

 rails g model user name:string email:string password_digest:string
 rake db:migrate

Dem Model fügen wir eine Zeile hinzu:

  class User < ActiveRecord::Base
    has_secure_password
    validates_presence_of :password, :on => :create
    attr_accessible :name, :email, :password, :password_confirmation
  end

Diese eine Zeile fügt sehr viel Automatik ein: Methoden zum Setzen und Verifizieren des eingegeben Passworts, validations. In der Datenbank wird dabei nie das Original-Passwort gespeichert, sondern nur eine gehashte Version. Damit sich neue User anmelden können brauchen wir einen Controller:

   rails g controller users

Der Code des Controllers ist kurz:

   class UsersController < ApplicationController
     def new
       @user = User.new
     end
     def create
       @user = User.new(params[:user])
       if @user.save
         redirect_to root_url, :notice => "Erfolgreich angemeldet!"
       else
         render "new"
       end
     end
   end

Und dann brauchen wir noch eine View zur Anmeldung:

   <h1>Anmeldung</h1>

    <%= form_for @user do |f| %>
      <% if @user.errors.any? %>
        <div class="error_messages">
          <h2>Fehler bei der Anmeldung</h2>
          <ul>
            <% for message in @user.errors.full_messages %>
              <li><%= message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
      <div class="field">
        <%= f.label :name %><br>
        <%= f.text_field :name %>
      </div>
      <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 %></div>
    <% end %>

Session, Login, Logout

Bearbeiten

Wie oben erwähnt ist die session ein Hash in Rails der in jedem Controller zugänglich ist, ähnlich wie params. Wir werden nun aber so tun, als ob sesssion ein model wäre, und eine View und einen Controller dafür anlegen.

Das Login-Formular wird as view sessions/new gespeichert. Dies ist das erste Formular, das nicht mit form_for erstellt wird, sonder mit from_tag. Der Unterschied: form_for ist immer an ein Model gebunden, mit form_tag kann man beliebige Formulare ganz unabhängig von Datenbank und Model, erzeugen.

  <h1>Login</h1>
  <%= form_tag sessions_path do %>
    <div class="field">
      <%= label_tag :email %>
      <%= text_field_tag :email, params[:email] %>
    </div>
    <div class="field">
      <%= label_tag :password %>
      <%= password_field_tag :password %>
    </div>
    <div class="actions"><%= submit_tag "Log in" %></div>
  <% end %>

Zu dieser View gehört ein Controller:

 rails g controller sessions

Der Controller hat 3 Actions, eine ist hier noch nicht vollständig implementiert:

   class SessionsController < ApplicationController
     def new
     end
     def create
       # ......
       if # .....
         session[:user_id] = user.id
         redirect_to root_url, :notice => "Logged in!"
       else
         flash.now.alert = "Falsche E-Mail or falsches Passwort"
         render "new"
       end
     end
     def destroy
       session[:user_id] = nil
       redirect_to root_url, :notice => "Logged out!"
     end
   end

Wenn das Login erfolgreich ist speichern wir die user_id in der Session - und nicht mehr.

Wie können wir nun prüfen ob der Login erfolgreich ist? Zuerst holen wir den user mit der Passenden E-Mail aus der Datenbank. Mit der authenticate-Methode des user-Objekts können wir prüfen ob das Passwort passt:

     user = User.find_by_email(params[:email])
     if user && user.authenticate(params[:password])

Damit wir in jedem Controller und jeder View die Information zur Verfügung haben ob jemand eingeloggt ist legen wir im applications_controller eine helper-Funktion an:

    class ApplicationController < ActionController::Base
      protect_from_forgery

      private

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

      helper_method :current_user
    end


Bearbeiten

Ähnlich wie hier in Wikibooks soll auch in der App tvtogether oben auf jeder Seite angezeigt werden ob mein eingloggt ist, bzw. Links für Login und Logout angeboten werden.

Das kann man in das Layout einfügen: app/views/layouts/application.html.erb

 <nav>
    <% if current_user.nil? %>
      <%= link_to "Login", new_session_path %>
    <% else %>
      Eingeloggt als <%= current_user.name %>.
      <%= link_to "Logout", session_path( session ), :method => :delete %>
    <% end %>
  </nav>


n:m Beziehung

In diesem Kapitel werden wir weiter an der App tvtogether arbeiten. Zwischen users und episodes besteht eine n:m-Beziehung: eine Person kann mehrere Episoden gesehen haben, eine Episode wurde von mehreren Personen gesehen.

  Dazu gibt es einen Railscast [9] HABTM Checkboxes (revised)

In einer relationalen Datenbank braucht man für die Darstellung einer n:m-Beziehung eine Zwischentabelle. Es es sinnvoll, aus dieser Zwischentabelle ein sogenanntes "Join Model" zu machen.

Join Model

Bearbeiten

Die drei beteiligten Modelle haben dabei folgende Beziehungen:

 class A < ActiveRecord::Base
   has_many :ABs
   has_many :Bs, through: :ABs
 end
 class AB < ActiveRecord::Base
   belongs_to :A
   belongs_to :B
 end
 class B < ActiveRecord::Base
   has_many :ABs
   has_many :As, through: :ABs
 end


Wie soll das Join-Modell in unserem Projekt heißen? hat_gesehen? seen? oder denken wir schon weiter zu einem Rating: meinung? rating? Ich habe mich dann für rating entschieden:

 rails g model rating user_id:integer episode_id:integer rating:integer
 rake db:migrate

So sehen die Models aus:

 class User < ActiveRecord::Base
   has_many :ratings
   has_many :episodes, :through => :ratings
   ...
 end
 class Ratings < ActiveRecord::Base
   belongs_to :user
   belongs_to :episode
   validates :rating, :inclusion => { :in => [0,1,2,3,4,5] }, :allow_nil => true
 end
 class Episode < ActiveRecord::Base
   has_many :ratings
   has_many :users, :through => :ratings
   ...
 end


Join Model auf der Console

Bearbeiten

Bevor wir mit dem Join-Model etwas programmieren, erforschen wir es interaktiv auf der Rails-Console:

 $ rails console
  Loading development environment (Rails 3.2.6)
  irb(main):001:0> u = User.first
    User Load (0.1ms)  SELECT "users".* FROM "users" LIMIT 1
    => #<User id: 1, name: "B", email: "b@gmail.com", password_digest: "$2a$10...">
  irb(main):002:0> e = Episode.last
    Episode Load (0.2ms)  SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT 1
    => #<Episode id: 1658, tvshow_id: 9, epnum: 195, seasonnum: 10, title: "To The Boy in the Blue Knit Cap">
  irb(main):003:0> u.ratings
    Rating Load (0.1ms)  SELECT "ratings".* FROM "ratings" WHERE "ratings"."user_id" = 1
    => [#<Rating id: 1, user_id: 1, episode_id: 1658, rating: nil>]
  irb(main):004:0> u.episodes
    Episode Load (0.2ms)  SELECT "episodes".* FROM "episodes" INNER JOIN "ratings" ON "episodes"."id" = "ratings"."episode_id" WHERE "ratings"."user_id" = 1
  => [#<Episode id: 1658, tvshow_id: 9, epnum: 195, seasonnum: 10, title: "To The Boy in the Blue Knit Cap">]
  irb(main):005:0> u.episode_ids
  => [1658]

Die letzte Schreibweise, mit den ids, kann man auch für Zuweisungen verwenden:

 irb(main):007:0> u.episode_ids = [1650, 1651, 1652]
    Episode Load (0.4ms)  SELECT "episodes".* FROM "episodes" WHERE "episodes"."id" IN (1650, 1651, 1652)
    (0.1ms)  begin transaction
    SQL (0.3ms)  DELETE FROM "ratings" WHERE "ratings"."user_id" = 1 AND "ratings"."episode_id" = 1658
    SQL (15.1ms)  INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?)
    SQL (0.1ms)  INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?)
    SQL (0.1ms)  INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?)
    (0.8ms)  commit transaction
    => [1650, 1651, 1652]

Hier werden also eventuell vorhandene Ratings gelöscht, und dann für die drei neuen Episoden Ratings eingefügt.

Controller und View

Bearbeiten

Wir bauen nun eine edit_user View, die immer eine Liste aller Episoden anzeigt. Bei jeder Episode gibt es eine Checkbox die für die Rating-Beziehung steht.

Hier ein erster Entwurf der View:

 <h1>Fernsehen mit <%= @user.name %></h1>
  <%= form_for(@user) do |f| %>
    <div class="field">
      <%= f.label :name %><br />
      <%= f.text_field :name %>
    </div>
    <div class="field">
      <% Episode.all.each do |ep| %>
        <label>
        <%= check_box_tag "user[episode_ids][]", ep.id %>
        <%= ep.tvshow.name %>
        <%= ep.epnum %>
        <%= ep.title %>
        </label><br/>
      <% end %>
    </div>
    <div class="actions">
      <%= f.submit %>
    </div>
  <% end %>

Die params die dieses Formular liefert sehen z.B. so aus:

  "user"=>{
     "name"=>"Brigitte",
     "episode_ids"=>["1666", "1668", "1706", "1707", "1781"]
   }

So werden die Checkboxes im user_controller verarbietet: die episode_ids müssen gar nicht extra behandelt werden:

 def update
   @user = User.find(params[:id])
   if @user.update_attributes(params[:user])
     redirect_to @user, notice: "Erfolgreich gespeichert"
   else
     render :edit
   end
 end

Nur im user-Model müssen sie als attr_accessible eingetragen werden:

 class User < ActiveRecord::Base
   has_many :ratings
   has_many :episodes, :through => :ratings, :order => "episodes.tvshow_id"
   has_secure_password
   validates_presence_of :password, :on => :create
   attr_accessible :name, :email, :password, :password_confirmation, :episode_ids
 end

Was das Formular noch nicht kann: die Checkboxes sind nie bereits angeklickt, auch nicht wenn in der Datenbank bereits ein Rating exisitert.

Bei Formularfelder die über den from_for Tag erzeugt ist das immer automatisch gegeben: Sie spiegeln den Zustand des Models wieder.

Diesen Checkboxes müssen wir "von Hand" setzen:

 <%= check_box_tag "user[episode_ids][]", ep.id, @user.episode_ids.include?(ep.id) %>


Anhang: Installation unter Windows

http://railsinstaller.org/


Anhang: Referenz

Weitere Quellen


Verwendete Quellen

Bearbeiten
  1. Rails Guide Layouts and Rendering
  2. Rails API AssetTagHelper
  3. Rails Guide Asset Pipeline
  4. Rails Guide Testing
  5. Hellesøy: Cucumber
  6. Rails Guide Active Record Associations
  7. Rails Guide Layouts and Rendering in Rails
  8. Railscast Authentication in Rails 3.1
  9. Railscast HABTM Checkboxes (revised)