Rapid Prototyping einer Web-Anwendung mit Ruby on Rails

Transcription

Rapid Prototyping einer Web-Anwendung mit Ruby on Rails
Westfälische Wilhelms-Universität Münster
Ausarbeitung
Rapid Prototyping einer Web-Anwendung
mit Ruby on Rails
im Rahmen des Seminars „Software Engineering“ im Wintersemester 2008/2009
Thomas Jansing
Themensteller: Prof. Dr. Herbert Kuchen
Betreuer: Dipl.-Wirt. Inform. Christian Hermanns
Institut für Wirtschaftsinformatik
Praktische Informatik in der Wirtschaft
Inhaltsverzeichnis
1 Einleitung ................................................................................................................... 1 2 Grundlagen von Ruby on Rails.................................................................................. 2 2.1 Allgemeine Konzepte und Ziele......................................................................... 2 2.2 Die Skriptsprache Ruby ..................................................................................... 3 2.3 Das Web-Framework Rails ................................................................................ 3 2.3.1 2.3.2 2.3.3 3 4 5 Eigenschaften und Besonderheiten ............................................................. 3 Aufbau des Frameworks ............................................................................. 5 RESTful Web-Services ............................................................................... 6 2.4 Eingesetzte Entwicklungsumgebung ................................................................. 6 2.5 Entwurf der Anforderungen ............................................................................... 7 Rapid Prototyping mit Ruby on Rails ........................................................................ 8 3.1 Vorstellung der Web-Anwendung ..................................................................... 8 3.2 Erstellung des Rails Projekts .............................................................................. 8 3.3 Testen der Funktionalität.................................................................................. 11 3.4 Web-Services und Routing .............................................................................. 12 Erweiterung des Projektes ....................................................................................... 14 4.1 Session-Handling und Benutzerverwaltung ..................................................... 14 4.2 Validierung in Modellen .................................................................................. 16 4.3 Templates und Layout ...................................................................................... 17 4.4 AJAX und Web 2.0 .......................................................................................... 18 4.5 E-Mail Versand durch ActionMailer ............................................................... 19 4.6 Sicherheitsaspekte ............................................................................................ 20 Zusammenfassende Betrachtung ............................................................................. 22 A Quelltexte ................................................................................................................. 23 Literaturverzeichnis ........................................................................................................ 38 II
Kapitel 1: Einleitung
1 Einleitung
Die vorliegende Ausarbeitung soll eine Einführung in das Web-Framework Rails geben,
welches auch synonym als Ruby on Rails bezeichnet wird, da es auf der Skriptsprache
Ruby aufbaut. Anhand der Entwicklung einer Beispiel-Web-Anwendung sollen die wesentlichen Elemente, sowie deren Vor- und Nachteile vorgestellt werden. Als Beispiel
hierfür wurde ein Pizza-Bestell-Service ausgewählt. Die Fokussierung auf Ruby on
Rails wurde gewählt, da es sich um ein relativ neues Framework handelt, welches durch
die Kombination verschiedener Ansätze versucht das „Agile Web Development“ und
speziell das „Rapid Prototyping“ zu unterstützen. Obwohl Ruby on Rails erst 2005 veröffentlicht wurde, wird es bereits in vielen produktiven Umgebungen (z. B. XING.com,
Qype.com und Eins.de) eingesetzt und erfreut sich nicht nur aufgrund seiner intuitiven
Syntax großen Interesses. Dank der Datenbank- und Plattform-Unabhängigkeit und der
Kompatibilität zum Apache- und lighttpd Webserver gewährleistet Ruby on Rails Interoperabilität für alle relevanten Web-Umgebungen. Die zugrunde liegende Sprache Ruby
ist sowohl Programmier- wie auch Skriptsprache und kann daher die Vorteile von eher
prozeduralen/imperativen Skriptsprachen wie dem weit verbreiteten PHP nutzen und
dabei trotzdem die Vorteile der Objektorientierung nutzen, da jedes Element in Ruby
ein Objekt ist. Ruby positioniert sich technologisch zwischen PHP/Perl auf der einen
Seite und Java und C# auf der anderen. Demnach ist Ruby ist vergleichbar mit Python.
Das folgende Kapitel 2 erläutert die Grundlagen von Ruby on Rails, wie die allgemeinen Konzepte und Ziele (Kap. 2.1), die Sprache Ruby (Kap. 2.2) und das Framework
Rails (Kap. 2.3) genauer. Kapitel 2.4 beschreibt die eingesetzte Entwicklungsumgebung, woraufhin in Kapitel 2.5 schließlich die Anforderungen an die Beispiel-WebAnwendung kurz erläutert werden. Kapitel 3 beschäftigt sich mit dem Thema „Rapid
Prototyping“ und beginnt nach einer kurzen Beschreibung der Anwendung (Kap. 3.1)
mit der Erstellung des Beispiel-Projektes (Kap. 3.2). Nachdem die Funktionalität in
Kapitel 3.3 gezeigt wurde, wird direkt die einfache Bereitstellung als Web-Service demonstriert (Kap. 3.4). Der erstellte Prototyp wird in Kapitel 4 um Funktionen erweitert,
wie ein Login-System (Kap. 4.1), Validierung (Kap. 4.2), angepasste Layouts und
Templates (Kap. 4.3), sowie eine AJAX-Live-Suche (Kap. 4.4). Das Kapitel schließt
mit der Betrachtung einiger Sicherheitsaspekte in Kapitel 4.6. Die Ausarbeitung endet
mit einer kritischen Zusammenfassung in Kapitel 5.
1
Kapitel 2: Grundlagen von Ruby on Rails
2 Grundlagen von Ruby on Rails
2.1 Allgemeine Konzepte und Ziele
Das Web-Framework Ruby on Rails wurde speziell darauf ausgerichtet, den Programmierer beim Rapid Development und im speziellen beim Rapid Prototyping zu unterstützen. Kapitel 3 wird dies exemplarisch zeigen. Ziele des Rapid Development sind u. a.
die Entwicklungsdauer und die dabei entstehenden Kosten zu senken. Die Programmierer können sich auf die Implementierung der Geschäftslogik konzentrieren und den
Nutzern schneller ein lauffähiges System demonstrieren. Dieses soll durch kürzere Entwicklungs-Zyklen erreicht werden und dafür sorgen, dass das Feedback der Nutzer
schneller in die Entwicklung einfließt. Dieser Ansatz verspricht mehr Agilität und Flexibilität bei der Entwicklung von Web-Anwendungen. Ruby on Rails unterstützt somit
das Extreme Programming (XP), indem es erlaubt die gesamte Web-Anwendung in
kleinen, iterativen Schritten zu entwickeln. So sind jederzeit Änderungen am Datenmodell möglich, ohne die Lauffähigkeit der Anwendung zu gefährden [WK07, S. 29].
Weiterhin unterstützt Ruby on Rails Test-driven development (TDD), indem es neben
dem lauffähigen Prototyp auch Tests für Modellklassen, Kontroller und Views, sowie
eine Fixture-Datei mit Testdaten generiert. In [WB06, Kap. 14] wird dieses ausführlicher besprochen. Beim Anlegen neuer Kontroller werden z. B. automatisch funktionale
Tests generiert, so dass bereits vor der ersten Anpassung des Codes die Funktionalität
getestet werden kann [WK07, S. 30]. Hierdurch kann mit dem Testen begonnen werden,
bevor der erste eigene Code implementiert wird. Ein wichtiger Vorteil der hieraus erwächst ist die Möglichkeit durch das Testen ganz genau das gewünschte Verhalten der
Anwendung zu spezifizieren und zu verifizieren [WK07, Kap. 4.3.1].
Ruby on Rails folgt strikt dem Model-View-Controller (MVC) Architekturmuster, welches 1979 von Trygve Reenskaug vorgestellt wurde. Dieses Muster hat sich seit langem
bewährt und verlangt eine strikte Trennung des Datenmodels (Model), der Präsentationsschicht (View) und der Steuerungsschicht (Controller) voneinander. Die Ziele hierbei sind die Web-Anwendung flexibel zu gestalten, um Wiederverwendung zur ermöglichen und einen möglichst geringen Pflegeaufwand zu haben, was zu einer geprüften
und stabilen Software beitragen soll [Wa08, Kap. 4.3]. Die genaue Umsetzung des
MVC-Musters in Rails wird in Kapitel 2.3.2 näher vorgestellt.
2
Kapitel 2: Grundlagen von Ruby on Rails
2.2 Die Skriptsprache Ruby
Die Open-Source Programmiersprache „Ruby“ wurde 1995 von dem Japaner Yukihiro
Matsumoto entwickelt und wird bis heute von ihm betreut. Ruby ist eine moderne Objekt-orientierte Skriptsprache und bietet daher Vorteile wie z. B. Modularisierung/Kapselung von Objekten, Vererbung/Spezialisierung und Polymorphismus. Im
Gegensatz zu anderen rein Objekt-orientierten Programmiersprachen wie z. B. Smaltalk
bietet Ruby darüber hinaus auch Unterstützung für Prozedurale und Funktionale Programmierung. So ist es bspw. nicht notwendig seine Programme explizit in einer Klasse
zu definieren. Ein Ruby-Programm kann auch lediglich aus Prozeduren oder Funktionen
bestehen. Sie unterstützt die dynamische Typisierung während der Laufzeit (DuckTyping) und folgt dem sogenannten Principle of least surprise, d. h. dass die Programmiersprache den Programmierer möglichst wenig überraschen sollte und sie intuitiv
verstanden werden sollte. Maßstäbe hierfür waren Matsumoto´s eigene Erwartungen
und Bedürfnisse. Ein positiver Aspekt von Ruby ist daher die gute Lesbarkeit des Codes. Ruby-Syntax ist gut verständlich, da die Programmiersprache sehr an die natürliche
menschliche Sprache angelehnt ist. Beispiele hierfür werden in Kap. 3 gegeben. Als
ausführliches Nachschlagewerk und Referenz für Ruby sei auf [CR2006] verwiesen.
2.3 Das Web-Framework Rails
2.3.1 Eigenschaften und Besonderheiten
Rails ist ein Open-Source Projekt und steht unter der MIT-Lizenz [Op08] zur Verfügung. Sie erlaubt eine absolut freie Verwendung von Rails. Das Framework wurde von
David Heinemeier Hansson entwickelt und, nachdem es 2004 bereits vorgestellt wurde,
Ende 2005 in der Version 1.0 freigegeben. Auffällig hierbei ist die Praxisnähe von
Rails, da es nicht als theoretisches Framework von Grund auf neu konzipiert wurde,
sondern aus den Erfahrungen, Mustern und Teilen der Implementierung der Projektmanagement-Software Basecamp zusammengestellt wurde. Daher berücksichtigt es konkrete Lösungen und erfolgreiche Muster aus einem Praxisprojekt, was wohl auch zu der
schnellen Verbreitung von Rails beiträgt [WK07, S. 21].
Eine Besonderheit von Rails ist sicherlich das Paradigma Konvention über Konfiguration, welches immer wieder am laufenden Fall-Beispiel verdeutlicht wird. Rails erwartet
3
Kapitel 2: Grundlagen von Ruby on Rails
vom Entwickler, dass er sich an Namenskonventionen hält, die eine spätere Konfiguration überflüssig machen. Beispielhaft sei aufgeführt, dass Klassennamen immer im Singular erwartet werden, während die Tabellennamen den Pluralnamen tragen. Sämtliche
generierten Dateien folgen diesem Schema. Rails bietet aber auch die Möglichkeit von
den Default-Einstellungen abzuweichen [WB06, S. 7 f.; WK07, S.28 f.].
Ein zweites Paradigma von Rails ist das DRY-Prinzip, welches von [TH04] geprägt
wurde. DRY steht für Don´t repeat yourself und baut auf dem Grundsatz auf, dass Wissen jeweils nur eine einzige und eindeutige Repräsentation in einem Informationssystem
haben sollte. Daher sollen weder Daten noch Funktionen redundant gespeichert werden,
um den Pflegeaufwand zu reduzieren und mögliche Probleme zu vermeiden. Ein Beispiel hierfür sind die Getter- und Setter-Methoden, welche nicht implementiert werden
müssen, da sie gleichartig sind und daher generiert werden können [Vgl. WB06, S. 8].
Besonders erwähnenswert ist die eigentliche Stärke von Rails, die Meta Programmierung. Die automatische Generierung von Programmcode wird bei Ruby on Rails Scaffolding genannt. Mit Hilfe von Scaffold-Generatoren wird direkt zu Beginn ein lauffähiges Gerüst erzeugt, welches bereits grundlegende Funktionen zum Erstellen, Anzeigen, Updaten und Löschen von Modellen (CRUD-Funktionen) implementiert. Zusätzlich werden auch die Views erzeugt, die eine Webbrowser-basierte Umsetzung der
CRUD-Funktionen ermöglichen. Somit kann sofort ein lauffähiger Prototyp erzeugt
werden, welcher als Grundlage für die weitere Entwicklung dient (Rapid Prototyping).
Ein Vorteil von Rails ist die Integration und besonders einfache Nutzung des Objektrelationen Mappings (ORM). Im Gegensatz zu anderen Frameworks wie z. B. MyFaces
ist bei Rails nur sehr wenig Aufwand hierfür nötig, da durch die Namenskonventionen
eine Konfiguration überflüssig ist. Nur Assoziationen zwischen Objekten müssen
(noch) manuell implementiert werden. Im Zusammenhang mit dem ORM sind die Datenbank-Migrationsskripte zu nennen. Diese automatisch generierten Skripte gewährleisten eine Kompatibilität zu allen verbreiteten Datenbanken und gestatten somit den
Betrieb von Rails mit einer Vielzahl an Datenbanken. Die Migrationen bieten darüber
hinaus den Vorteil, dass eine Versionierung von Datenbank-Schemata möglich ist. Es
können neue Schemata migriert oder auch auf ältere zurückgekehrt werden.
Rails bietet drei verschiedene Umgebungen mit jeweils eigenen Datenbanken. Standard
ist die Development-Umgebung, wohingegen die Production-Umgebung für den Produktiv-Einsatz vorgesehen ist. Alle Tests werden in der Test-Umgebung mit Test-Daten
4
Kapitel 2: Grundlagen von Ruby on Rails
ausgeführt. Diese Trennung der Daten erlaubt bspw. das Testen von Änderungen am
Datenbank-Schema, ohne auf Daten der Produktiv-Umgebung zuzugreifen und diese
möglicherweise zu verändern oder zu löschen.
2.3.2 Aufbau des Frameworks
Abbildung 1 zeigt die Komponenten von Rails und deren Interaktion sowie die konkrete
Umsetzung des MVC-Musters. Der Ruby-Dispatcher im Zentrum erhält vom WebServer die Anfrage des Clients. Der Dispatcher lädt daraufhin anhand des Routings
(config\routes.rb) den entsprechenden Controller des ActionController Moduls.
Der Controller verwaltet die Anfrage und ruft die entsprechenden Methoden des ActiveRecord oder ActiveRessource Moduls auf. ActiveRecord interagiert mit der Datenbank als OR-Mapper und enthält die Logik zur Verwaltung der Daten. ActiveRessource
(ab Rails Version 2.0) hingegen ist für RESTful Web-Service-Anfragen und nutzt ActiveRecord als ORM. Die Ergebnisse der Aufrufe werden an den Controller zurückgeschickt und dann vom Controller an das ActionView Modul gesendet. Das ActionView
Modul rendert die Ergebnisse und liefert sie an den Client zurück.
Abbildung 1: Komponenten von Rails (ab Version 2.x)
Zusätzlich können ActionController, ActiveRecord und -Ressource auf ActiveSupport
zugreifen, welches Erweiterungen und Hilfsmethoden für Rails implementiert. Daneben
5
Kapitel 2: Grundlagen von Ruby on Rails
gibt es das ActionMailer Modul, welches den E-Mail-Versand übernimmt. Alle Action
Module werden auch unter dem Begriff ActionPack zusammengefasst.
2.3.3 RESTful Web-Services
Das Representational State Transfer (REST) Architektur-Muster wurde von [Fi00] für
verteilte Hypermedia-Systeme, wie z. B. das World Wide Web, beschrieben und wird
ausführlich in [Fi00, Kap. 5] behandelt. In den Versionen 1.x von Rails wurde das Modul Action Web Service als Standard für die Bereitstellung von Web-Services vorgesehen. Das Modul bietet Unterstützung für SOAP- und XML-RPC-Zugriffe. Mit der Rails
Version 2.0 wurde das Modul durch ActiveRessource ersetzt, welches Web-Services
über REST unterstützt. Eine ausführliche Behandlung des Themas ist in [RR07] nachzulesen. Action Web Service kann über RubyGems nachinstalliert werden, aber die
Rails Entwickler empfehlen SOAP nicht mehr einzusetzen und REST zu nutzen. Rails
2.0 ist bereits RESTful konzipiert, was bereits beim Prototyping deutlich wird.
2.4 Eingesetzte Entwicklungsumgebung
Für die Entwicklung der Beispiel-Web-Anwendung wurde als Betriebssystem Windows
XP benutzt, so dass bei allen folgenden Befehlen in der Eingabeaufforderung ein „\“
anstelle eines „/“ als Pfadtrenner benutzt wurde. Entsprechend der Empfehlung der
rubyonrails.com Webseite wurde das Ruby-Paket in der Version 1.8.6-27 RC 1
[Ruby08a] verwendet. Nach erfolgter Installation von Ruby („RubyGems Support“ und
„European Keyboard“ müssen aktiviert werden) wurde mit Hilfe der mitgelieferten RubyGems Paket-Verwaltung das Rails-Paket (Ver. 2.1.1), der SQLite3 Adapter (Ver.
1.2.3) und der Mongrel Web-Server (Ver. 1.1.5) heruntergeladen:
“gem install rails --version 2.1.1”
“gem install sqlite3-ruby --version 1.2.3”
“gem install mongrel --version 1.1.5”
Die vorliegende Rails Version besitzt leider eine bekannte Inkompatibilität unter Windows XP mit allen MySQL-Servern, daher wurde SQLite3 als Datenbank benutzt. Um
SQLite3 zu installieren wurden die „Precompiled binaries“ für Windows heruntergeladen (sqlite-3_6_4.zip und sqlitedll-3_6_4.zip [Sqlite08]) und anschließend
in das \bin Verzeichnis der Ruby-Installation extrahiert, damit sie über die PATHVariable in allen Verzeichnissen ausführbar sind. Für die Integration anderer Datenban6
Kapitel 2: Grundlagen von Ruby on Rails
ken wird jeweils ein Adapter benötigt, welcher auch in der DB-Konfiguration des Projekts angegeben werden muss (config\database.yml). Ausführliche Hinweise zur
Installation und Konfiguration von Datenbanken bietet das offizielle Ruby on RailsWiki [Ruby08b].
2.5 Entwurf der Anforderungen
Mit Fokus auf das Extreme Programming wird hier auf eine Analyse- und Spezifikations-Phase sowie auf ein Pflichten- und Lastenheft im Sinne des klassischen Software
Engineering verzichtet. Es wird direkt mit einer textuellen Formulierung der Anforderungen begonnen, auf deren Basis ein UML-Diagramm (Vgl. Abbildung 2) erstellt wird.
Das Projekt pizza-service soll einen Pizza-Bestellservice implementieren. Es soll
eine Pizza-Verwaltung (Klasse: Pizza) für einen Admin (Klasse: User), sowie eine
grundlegende Bestell-Funktionalität (Klasse: Order) für alle Kunden (Klasse: User) erstellt werden. Kunden sollen sich nach einer Registrierung einloggen können, um nicht
zu jeder Bestellung nochmals ihre Adresse angeben zu müssen. Außerdem können sie
ihr Profil jederzeit ändern. Ein Admin soll sich über einen Login autorisieren und die
Pizzen verwalten können. Bestellungen von Kunden werden per E-Mail an den Lieferservice gesendet. Weiterhin soll eine Suchfunktion implementiert werden, um nach Pizzen mit bestimmten Zutaten zu suchen. Diese Suchfunktion wird einerseits als RESTful
WebService und zusätzlich als AJAX-Live-Suche implementiert.
Abbildung 2: Vereinfachtes UML-Diagramm der Beispiel-Web-Anwendung
Einem User können mehrere Bestellungen zugeordnet werden. Eine Bestellung ihrerseits wird genau einem User zugeordnet und kann aus mehreren Pizzen bestehen. Pizzen
können in mehreren Bestellungen enthalten sein. Diese n:m Beziehung wird hierbei
durch die Assoziationsklasse Orders_Pizzas abgebildet. Get- und Set-Methoden,
sowie IDs und Timestamps sind nicht angegeben, da sie automatisch generiert werden.
Die Attribute werden als SQLTypes definiert.
7
Kapitel 3: Rapid Prototyping mit Ruby on Rails
3 Rapid Prototyping mit Ruby on Rails
3.1 Vorstellung der Web-Anwendung
Im Rahmen dieses Kapitels wird das Rapid Prototyping mit Rails verdeutlicht. In diesem Kontext werden Klassen synonym als Modelle und Methoden als Aktionen bezeichnet. In einem ersten Schritt sollen die Modelle erstellt und deren Verwaltung
(CRUD-Operationen) in Views ermöglicht werden. Außerdem sollen die Assoziationen
der Modelle berücksichtigt werden. Die Funktionalität wird anhand der Ruby-Konsole
überprüft. Anschließend wird eine einfache Suchfunktion als WebService implementiert
(Kapitel 3). Als zweiter Schritt soll der Prototyp um eine Benutzerverwaltung erweitert
werden, um die Bestellung für registrierte Kunden zu vereinfachen und nur einem Admin die Verwaltung von Pizzen zu gestatten. Nachdem die Validierung und das Arbeiten mit Templates gezeigt werden, werden die AJAX-Live-Suche und ein automatischer
E-Mail-Versand implementiert und Sicherheitsaspekte besprochen (Kapitel. 4). Da sich
die Ausarbeitung auf die Umsetzung der Anwendung konzentriert, wird hier teilweise
auf eine komplette Auflistung der Änderungen an den View-Templates verzichtet und
auf den Anhang verwiesen.
3.2 Erstellung des Rails Projekts
Nachdem die Entwicklungsumgebung in Kapitel 2.4 eingerichtet wurde, wird als erster
Schritt das neue Rails Projekt pizza-service erstellt. Dieses geschieht über folgenden Befehl in der Eingabeaufforderung in einem beliebigen Verzeichnis:
“rails pizza-service”
“cd pizza-service”
Rails erstellt automatisch im Verzeichnis pizza-service eine festgelegte Verzeichnisstruktur (Vgl. Tabelle 1) und erzeugt alle benötigten Dateien, wodurch der grundsätzliche Aufbau der Web-Anwendung bereits vorgegeben ist. Dieses reduziert den Einarbeitungsaufwand bei zukünftigen Projekten. Jede Rails-Anwendung verwendet die gleiche Struktur, wodurch eine spätere Konfiguration, wie im J2EE-Bereich üblich, vermieden wird. Hierbei wird nochmal das Prinzip „Konvention statt Konfiguration“ deutlich.
Verzeichnis
app
Inhalt
Alle wichtigen Bestandteile des MVC-Musters: Hilfs-, Kontroller8
Kapitel 3: Rapid Prototyping mit Ruby on Rails
config
db
doc
lib
log
public
script
test
tmp
vendor
und Modell-Klassen, sowie sämtliche Views (*.html.erb)
Konfigurationsdateien (z. B. database.yml)
DB-Migrationsskripte, SQLite3 Datenbank-Datei (nach Anlegen)
Dokumente, die mit rdocs erstellt wurden
Zusätzliche Ruby-Bibliotheken/Erweiterungen
Logfiles des WEBrick/Mongrel-Servers
Öffentliches Wurzel-Verzeichnis des Web-Servers mit statischen Dateien (z. B. Bilder, JavaScript, CSS, ...)
Start-Skripte (z. B. Server-Start, DB-Konsole, Generatoren)
Test-Skripte, Unit-Tests, Funktionale Tests sowie Integrationstests
Temporäre Dateien (z. B. Session-Dateien, Cache-Dateien)
Plug-Ins und andere Erweiterungen
Tabelle 1: Verzeichnisstruktur einer Rails-Anwendung
Nun wird das UML-Diagramm (Vgl. Abbildung 2) mit Hilfe des Scaffold-Generators
umgesetzt. Die drei Modelle Pizza, Order und User werden erstellt, wobei Rails automatisch sämtliche Controller, Views, Tests und DB-Migrationsskripte erzeugt. Die
Assoziationsklasse Orders_Pizzas wird nicht als Modell umgesetzt, sondern später
nur als SQL-Tabelle. Die Attribute werden aus dem UML-Diagramm übernommen:
“ruby script\generate scaffold pizza name:string price:decimal
ingredients:string”
“ruby script\generate scaffold order delivery_wish:text”
“ruby script\generate scaffold user name:string address:string
zip_code:integer city:string login:string password:string”
In der eingesetzten Rails-Version ist es (noch) nicht möglich, Assoziationen automatisch zu berücksichtigen. Dazu werden die leeren generierten Klassen wie folgt ergänzt:
class Pizza < ActiveRecord::Base
# Einer Pizza werden 0..* Bestellungen zugeordnet
has_and_belongs_to_many :order
end
Listing 1: app\models\pizza.rb
class Order < ActiveRecord::Base
# Einer Bestellung werden 0..* Pizzen zugeordnet
has_and_belongs_to_many :pizza
# Eine Bestellung wird genau einer Person zugeordnet
belongs_to :user
end
Listing 2: app\models\order.rb
class User < ActiveRecord::Base
# Einem User werden 0..* Bestellungen zugeordnet
has_many :order
end
Listing 3: app\models\user.rb
9
Kapitel 3: Rapid Prototyping mit Ruby on Rails
Als nächster Schritt muss das OR-Mapping konfiguriert werden. Durch den ScaffoldGenerator sind bereits die DB-Migrationsskripte (db\migrate\*.rb) erstellt worden
und beinhalten bereits alle oben angegeben Attribute. Daher müssen sie nur noch an die
Assoziationen angepasst werden. Um die Zuordnung der Bestellung zu genau einem
User zu ermöglichen, wird deren Primärschlüssel als Fremdschlüssel in der ordersTabelle gespeichert. Um die many-to-many-Beziehung zwischen Bestellung und Pizza
abbilden zu können, muss eine neue Tabelle orders_pizzas angelegt werden. Diese
Tabelle braucht keinen eigenen Primärschlüssel und beinhaltet nur die beiden Fremdschlüssel, um die Relation abzubilden. Als Konvention für den Namen einer zusätzlichen Tabelle einer many-to-many-Beziehung gilt, dass beide pluralisierten Tabellennamen alphabetisch angeordnet und durch einen Unterstrich verbunden werden.
class CreateOrders < ActiveRecord::Migration
def self.up # Neues Schemata wird migriert
create_table :orders do |t|
t.text :delivery_wish
t.integer :user_id # Fremdschlüssel wird ergänzt
t.timestamps
end
# Hilfstabelle wird angelegt. Keine eigene id.
create_table :orders_pizzas, :id => false do |t|
t.integer :order_id # Fremdschlüssel der Bestellungen
t.integer :pizza_id # Fremdschlüssel der Pizzen
end
end
def self.down # Alte Tabellen/Schemata werden gelöscht
drop_table :orders
drop_table :orders_pizzas # Hilfstabelle entfernen
end
end
Listing 4: db\migrate\timestamp_create_orders.rb
Da die anderen Migrationen nicht geändert werden, kann jetzt die Datenbank aktualisiert werden. Weil SQLite 3 als Datenbank benutzt wird, muss keine Änderung an der
Datei config\database.yml vorgenommen werden. Mit dem Rake-Tool können
vordefinierte Aufgaben, wie z. B. die Datenbank-Migration ausgeführt werden. Eine
komplette Übersicht ist unter [MO08, Kap. 7.11] zu finden. Rake ist daher mit den
Tools ant bzw. make zu vergleichen [St08, S. 138]. Folgender Befehl migriert das
Schemata in die Datenbank:
“rake db:migrate”
Die Development-Datenbank-Datei db\development.sqlite3 wird erstellt und vier
neue Tabellen werden angelegt.
10
Kapitel 3: Rapid Prototyping mit Ruby on Rails
3.3 Testen der Funktionalität
Für einen direkten Test der Funktionalität werden die Ruby-Konsole und die Views
benutzt. Für das Testen mit Hilfe von Testfällen und Testskripten sei hier auf [WB06,
Kap. 14] verwiesen. Als Webserver für die Entwicklung kann der integrierte WEBrickServer verwendet werden. Eine bessere Performance bietet Mongrel, welcher in Produktiv-Umgebungen eingesetzt wird. Das Server-Start-Skript überprüft ob Mongrel
installiert ist und startet standardmäßig diesen (andernfalls wird WEBrick gestartet):
“ruby script\server”
Unter der URL http://localhost:3000 kann die Startseite des Web-Servers erreicht werden. Da Ruby on Rails als CRUD-Framework ausgelegt ist, sind durch das
Scaffolding bereits alle nötigen Views und die gesamte Logik vorhanden, um die drei
Modelle zu verwalten. Die Index-Seite für deren Verwaltung ist nach folgendem Schema aufgebaut: http://localhost:3000/modelname_im_plural. Als hilfreich
zum Testen der der generierten Namen hat sich der Pluralizer [Nu08] erwiesen. Es ist
jeweils möglich Instanzen der Modelle anzulegen, anzuzeigen, zu editieren und zu löschen. Über die Ruby-Konsole werden Instanzen von Modellen angelegt, die durch Zuweisungen miteinander assoziiert werden, um die Funktionalität der Assoziationen zu
demonstrieren. Zuerst werden über die Konsole drei Pizzen und zwei Kunden angelegt:
“ruby script\console”
pizza1 = Pizza.new(:name => "Pizza Margherita", :price => 4.95,
:ingredients => "Tomaten, Kaese")
pizza2 = Pizza.new(:name => "Pizza Salami", :price => 5.45,
:ingredients => "Tomaten, Kaese, Salami")
pizza3 = Pizza.new(:name => "Pizza Tono", :price => 5.95,
:ingredients => "Tomaten, Kaese, Thunfisch")
user1 = User.new(:name => "Max Mustermann", :address => "Hansaring 55", :zip_code => 48143, :city => "Muenster")
user2 = User.new(:name => "Peter Muster", :address => "Wolbecker Str. 55", :zip_code => 48143, :city => "Muenster")
pizza1.save
pizza2.save
pizza3.save
user1.save
user2.save
Listing 5: Eingaben in der Ruby-Konsole (1/3)
Eine Kontrolle im Webbrowser zeigt, dass alle Instanzen angelegt wurden. Nun wird
eine Bestellung erzeugt, die Kunde 1 zugeordnet wird und Pizza 2 & 3 enthält:
11
Kapitel 3: Rapid Prototyping mit Ruby on Rails
order1 = Order.new(:delivery_wish => "Bitte noch 1 Liter Cola")
user1.order << order1 # Bestellung wird User zugeordnet (1:m).
order1.pizza << pizza2 #.pizza ist ein Array, wg. n:m Beziehung
order1.pizza << pizza3
order1.save
Listing 6: Eingaben in der Ruby-Konsole (2/3)
Eine Kontrolle der DB-Datei bspw. durch den SQLite3 Manager zeigt, dass die Tabelle
oders_pizzas korrekt beide Pizzen der Bestellung zuordnet. Auch der Fremdschlüs-
sel user_id in der Tabelle orders zeigt korrekt die Assoziation mit dem Kunden. Ein
letzter Beweis geben folgende Ruby-Konsolen-Befehle:
bestellung = Order.find(:first)
bestellung.user.name
# Alle Pizzen werden ausgelesen
bestellung.pizza.each { |pizza|
# Suche nach der 1. Bestellung
# Gibt den Namen des Kunden aus
und deren Namen angezeigt
puts pizza.name }
Listing 7: Eingaben in der Ruby-Konsole (3/3)
Das Anlegen der Pizzen und Kunden ist vollständig im Webbrowser möglich, lediglich
bei der Bestellung kann nur der delivery_wish angegeben werden. Damit eine Zuweisung einer Bestellung zu einen Kunden und mehrerer Pizzen zu einer Bestellung
möglich wird, werden der OrdersController und die beiden Order-Views
new.html.erb und index.html.erb überarbeitet (Vgl. Anhang A).
3.4 Web-Services und Routing
Wie in Kapitel 2.3.3 bereits erläutert bietet Rails ab der Version 2.0 eine Unterstützung
für RESTful Web-Services. Für die Kommunikation zwischen Anwendungen wird
hierbei das XML-Format unterstützt. Bei dem REST-Architektur-Muster stehen anstelle
von Services Ressourcen im Vordergrund, welche durch URLs adressiert und durch
HTTP-Requests kontrolliert werden. Ein paar Beispiele zur Verdeutlichung:
GET http://localhost:3000/pizzas.xml #XML-Darstel. aller Pizzen
POST http://localhost:3000/pizzas.xml
#Erstellt neues Objekt
GET http://localhost:3000/pizzas/1.xml #Liefert XML-Objekt(id 1)
PUT http://localhost:3000/pizzas/1.xml
#Ändert Objekt (id 1)
DELETE http://localhost:3000/pizzas/1
#Löscht Objekt (id 1)
Mit Hilfe der vier HTTP-Requests (POST, GET, PUT, DELETE) können somit
CRUD-Operationen durchgeführt werden. Zum Testen der REST-Anfragen empfiehlt
sich das Firefox-Plugin „RESTTest“ [Xu08]. Dazu wird die protect_from_forgery
Methode im ApplicationController temporär auskommentiert, welche sonst nur
12
Kapitel 3: Rapid Prototyping mit Ruby on Rails
GET und POST-Anfragen zulässt und PUT und DELETE blockiert. Die durch den
Scaffold-Generator erzeugten Controller implementieren RESTful Web-Services bereits
über das Routing. Die Datei config\routes.rb ist dabei zentral für das Routing,
welches für einen HTTP-Request den verantwortlichen Controller und die auszuführende Aktion auswählt. Die Bereitstellung als Ressource ist bereits über den Eintrag
(map.resources
:pizzas)
implementiert.
(http://localhost:3000/pizzas/edit/1)
Der
wird
wie
Aufruf
(PUT
http://localhost:3000/pizzas/1) an den PizzasController weitergeleitet
und ruft die Aktion edit für das Objekt mit der ID 1 auf. Bei der Erstellung komplexerer RESTful Web-Services sollte auf konsistente Adressierung geachtet werden. Das
Projekt wird nun um einen RESTful Web-Service search_all erweitert, der die Zutaten-Spalte der Pizza-Tabelle durchsucht und im PizzasController implementiert
wird. Gleichzeitig wird das Routing um den neuen Web-Service ergänzt:
# GET /pizzas/search_all
# GET /pizzas/search_all.xml
def search_all
#Suchbegriff wird für die SQL Abfrage mit % geklammert
@term = "%#{params[:term]}%"
#Suchanfrage wird ausgeführt. SQL-Injection wird vermieden.
@pizzas = Pizza.find(:all, :conditions =>
["ingredients like ?", @term ])
#Rendert das Ergebnis nur als XML, kein HTML, kein AJAX
respond_to do |format|
format.xml { render :xml => @pizzas }
end
end
Listing 8: Neue Methode in: app\controllers\pizzas_controller.rb
# “map.ressources :pizzas” wird geändert in:
map.resources :pizzas, :collection => {:search_all => :get}
Listing 9: Erweiterung von: config\routes.rb
Folgende GET-Anfrage eines Webbrowsers bspw. liefert eine XML-Liste aller Pizzen,
die „Salami“ als Zutat haben (Web-Server muss neu gestartet werden):
“http://localhost:3000/pizzas/search_all?term=salami”
SQL-Injection (Vgl. Kap. 4.6) ist hierbei nicht möglich, da die Binding-Funktionalität
benutzt wird. Da REST-Anfragen stark sicherheitsrelevant sind, ist eine ausführliche
Beschäftigung mit [Ruby08d] anzuraten.
13
Kapitel 4: Erweiterung des Projektes
4 Erweiterung des Projektes
4.1 Session-Handling und Benutzerverwaltung
Mit Hilfe der Attribute login und password des User-Modells wird nun eine einfache Benutzerverwaltung implementiert. Weiterhin wird dazu der Session-Hash und
Flash-Hash benutzt. Der Session-Hash speichert Objekte über mehrere Requests hinweg, und wird als Speicher für den angemeldeten User (bzw. User-Objekt) benutzt. Auf
die Objekte wird hierbei mit der session Variable zugegriffen. Der Flash-Hash hingegen speichert Objekte hingegen nur für zwei aufeinander folgende Requests und wird
für Fehlermeldungen sowie Hinweise und Warnungen verwendet. Die Informationen
werden hierbei in der flash Variable gespeichert [St08, S.180].
Für die Benutzerverwaltung (Vgl. [St08, Kap. 4.6.3]) wird ein neuer LoginController mit den Aktionen login und logout generiert und angepasst:
“ruby script\generate controller Login login logout”
class LoginController < ApplicationController
def login
session[:user] = nil # User-Objekt und Werte löschen
session[:user_id] = nil
session[:user_login] = nil
# Login-Button löst POST-Request aus
if request.post?
# Authentifiziere User -> app\models\user.rb
user = User.authenticate(params[:login],
params[:password])
# Konnte User-Objekt erfolgreich geladen werden?
if user != nil # Wenn ja setze neues Objekt und Werte
session[:user] = user
session[:user_id] = user.id
session[:user_login] = user.login
# Hinweis und Redirect
flash[:notice] = "Successfully logged in!"
redirect_to :controller => "pizzas", :action => "index"
else # Fehler bei der Authentifizierung
flash[:error] = "Login or passwort are incorrect!"
end
end
end
def logout
reset_session # Löscht alle Session Objekte des Users
end
end
Listing 10: app\controllers\login_controller.rb
14
Kapitel 4: Erweiterung des Projektes
Die eigentliche Authentifizierung soll im Modell geschehen. Daher wird das UserModell um die Methode authenticate erweitert, die das User-Objekt anhand des
login Parameters lädt und das übergebene Passwort mit dem gespeicherten vergleicht:
def self.authenticate(login, password)
user = self.find_by_login(login) # Lade User-Objekt
if user # User-Objekt erfolgreich geladen, prüfe Passwort
if user.password != password
user = nil
# Passwort falsch. Gib Fehler zurück (NIL)
end
end
user
# Passwort richtig. Gib User-Objekt zurück
end
Listing 11: Neue Methode in: app\models\user.rb
Nun kann der Zugriff auf die einzelnen Aktionen der Controller geschützt werden. Dazu
werden die Controller um Filter erweitert, die vor (before_filter) oder nach (after_filter) jeder Aktion ausgeführt werden:
# Delete ist admin-geschützt, Anzeigen und Anlegen nicht
before_filter :admin_logged_in, :only => [:delete, :destroy]
# User dürfen nur ihre eigenen Profile ändern
before_filter :logged_in, :own_profile,:only =>[:edit, :update]
Listing 12: Erweiterung von: app\controllers\users_controller.rb
# Delete/Edit sind admin-geschützt, Anzeigen nicht
before_filter :admin_logged_in, :only => [:delete, :destroy,
:edit, :update]
# Um Bestellungen anzulegen muss User eingeloggt sein
before_filter :logged_in, :only => [:create, :new]
Listing 13: Erweiterung von: app\controllers\orders_controller.rb
# Create/Edit/Delete sind admin-geschützt, Anzeigen und Suchen
nicht
before_filter :admin_logged_in, :except => [:index, :list,
:show, :search_all]
Listing 14: Erweiterung von: app\controllers\pizzas_controller.rb
Die Methoden admin_logged_in, logged_in und own_profile werden im ApplicationController als Private-Methoden implementiert, damit sie über die Verer-
bung allen Controllern zur Verfügung stehen:
private
# Überprüft ob User eingeloggt ist
def logged_in
if session[:user] == nil
flash[:error] = "You need to login first!"
redirect_to(:action => 'index')
end
15
Kapitel 4: Erweiterung des Projektes
end
private
# Überprüft ob User sein eigenes Profil editiert
def own_profile
if session[:user].id.to_s != params[:id]
flash[:error] = "You can only edit your own profile."
redirect_to(:action => 'index')
end
end
private
def admin_logged_in
# Überprüft ob user.login = admin
logged_in
if session[:user] != nil
if session[:user].login.to_s == "admin"
flash.now[:notice] = "You have admin rights."
else
flash[:error] = "You need to have admin rights to use
this function."
redirect_to(:action => 'index')
end
end
end
Listing 15: Neue Methoden in: app\controllers\application.rb
Nachdem die generierten Views für den Login und den Logout angepasst wurden
(Vgl. Anhang A), steht die Benutzerverwaltung zur Verfügung:
“http://localhost:3000/pizzas”
“http://localhost:3000/login/login”
“http://localhost:3000/login/logout”
“http://localhost:3000/users/new”
#
#
#
#
Pizza-Service Startseite
Anmeldeseite
Abmeldung
Registrierung neuer User
4.2 Validierung in Modellen
Die Validierungsfunktionalität ist fester Bestandteil von ActiveRecord und findet somit
auf Modellebene statt. Es kann entweder eine Klassen-Methode validate in die Modell-Klasse eingefügt werden, um eigene Validierungen zu implementieren, oder auf
Standard-Methoden von ActiveRecord zurückgegriffen werden, die als KlassenMethoden eingefügt werden. Nachfolgend wird das Pizza-Modell um verschiedene Validierungen erweitert:
# Alle drei Attribute müssen angegeben werden
validates_presence_of :name, :price, :ingredients
# Das Attribut “Name” der Pizza soll eindeutig sein
validates_uniqueness_of :name
# Das Attribut “Preis” soll positiv sein
validates_numericality_of :price, :on => :create,
:greater_than => 0, :message => "Price must be > 0."
Listing 16: Erweiterung von: app\models\pizza.rb
16
Kapitel 4: Erweiterung des Projektes
Der Parameter :on definiert wann die Validierung durchgeführt werden soll (hier beim
Anlegen) und :message überschreibt die eventuell ausgegebene Fehlermeldung (vor
allem bei numerischen Prüfungen wichtig). Als Standard werden die Validationen bei
jedem Ändern (:save) des Objekts ausgeführt. Standard-Fehlermeldungen sind bereits
definiert. Die Modelle User und Order werden ebenfalls um sinnvolle Validationen
erweitert (Vgl. Anhang A). Eine gute Übersicht und Beschreibung aller vorhandenen
Standard-Validationen bieten [MR06, Kap. 5.6.6].
4.3 Templates und Layout
Rails verfügt über drei eingebaute Template-Engines, die Teil des ActionView-Moduls
sind (ERb-, XML- und Ruby JavaScript-Templates). Anhand der Dateiendung erkennt
Rails, welche Engine zu wählen ist. Ab Rails 2.0 werden als Standard ERB-Templates
(app\views\*\*.html.erb) vom Scaffold-Generator erzeugt. Embedded Ruby
(ERb)-Templates sind einfache HTML-Templates mit über Tags eingebettetem RubyCode. Tabelle 2 gibt hierüber einen Überblick (Vgl. [MO08, Kap. 8]).
Tag
<% RUBY_CODE %>
<%- RUBY_CODE -%>
<%= RUBY_CODE %>
<%=h RUBY_CODE %>
<%# RUBY_CODE %>
Beschreibung
Code wird ausgewertet, keine Ausgabe
Code wird ausgewertet, keine Ausgabe, unterdrückt <br>
Code wird ausgewertet und als String ausgegeben
Code wird ausgewertet, Resultat wird durch html_escape
gefiltert (Vgl. Kap.4.6)
Code wird nicht ausgewertet (Kommentar)
Tabelle 2: Tags zum Einbinden von Ruby-Code in ERb-Templates
Da nach dem MVC-Muster die Controller die Views steuern, muss zum Rendern der
Views innerhalb einer Aktion eines Controllers die render-Funktion aufgerufen werden, der als Parameter der Name des Templates übergeben wird. Aufgrund der Namenkonvention wird Rails die Endung .html.erb hinzufügen. Allgemein:
Render :action => „Template_name“
Bei :action stehen dem Controller nur Templates aus dem eigenen Unterordner zur
Verfügung. Über :template kann ein relativer Pfad und über :file ein absoluter
Pfad zu anderen Templates angegeben werden [MR06, S. 68]. Im Ordner
app\views\layouts befinden sich spezielle Templates, die nach dem DRY-Prinzip
als gemeinsames Layout für alle Seiten eines Controllers dienen (controller.hmtl.erb). Wird die Datei application.html.erb angelegt, dient diese als
17
Kapitel 4: Erweiterung des Projektes
Layout für alle Seiten der Anwendung. Das Projekt wird um ein gemeinsames Layout
erweitert, dass alle Funktionen und Seiten zusammenführt (Vgl. Anhang A).
4.4 AJAX und Web 2.0
„Asynchronous JavaScript and XML“ (AJAX)-Funktionalität ist fester Bestandteil von
Ruby on Rails. Hierfür wurden die beiden AJAX-Frameworks Prototype und
Script.aculo.us integriert. Eine ausführliche Darstellung von AJAX und den beiden
Frameworks wird in [Ga07, Kap.6 & 7] gegeben. Die JavaScript Dateien befinden sich
unter public\javascripts und werden in den Views wie folgt eingebunden:
<%= javascript_include_tag :defaults %>
Dadurch werden alle relevanten JavaScript-Dateien geladen (Prototype, Script.aculo.us,
sowie application.js, die selbst erstellte JavaScripts enthält). Diese Skripte können
nun in den Views verwendet werden um AJAX zu realisieren, wodurch aber eine Vermischung von HTML, Ruby und JavaScript auftritt. Um diese zu vermeiden und einheitlich nur HTML und Ruby-Code zu schreiben, kann der Ruby JavaScript (RJS)Generator benutzt werden. Dieser übersetzt Ruby-Code zur Laufzeit in JavaScript und
ermöglicht eine einheitliche Programmierung in Ruby. Da JavaScript zum View im
MCV-Muster gehört, ist es besser, den RJS-Code in Templates auszulagern, die den
Namen der aufzurufenden Action und die Endung .js.rjs tragen.
Nachdem die search_all Methode in Kap. 3.4 bereits als WebService über XML
realisiert wurde, wird diese nun um einen HTML-View mit Live-Search-Funktion erweitert, um die einfache Einbindung von AJAX zu demonstrieren:
<%# Die AJAX-Suche wird als Partial-View eingebunden %>
<%= render :layout => false, :partial => 'live_search' unless
request.xhr? %> <%# Namenskonvention: _live.search.html.erb%>
<%# Bei Ergebnisse erzeuge Tabelle%>
<div id="live_results">
<% unless @pizzas.empty? %>
<h3>Search results</h3>
<table width="500" >
<tr><th width="200">Name</th>
<th width="100">Price</th>
<th width="200">Ingredients</th></tr>
<% for pizza in @pizzas %>
<tr><td><%=h pizza.name %></td>
<td><%=h pizza.price %></td>
<td><%=h pizza.ingredients %></td></tr>
<% end %>
</table>
18
Kapitel 4: Erweiterung des Projektes
<% else %><%# Wenn keine Ergebnisse vorliegen %>
<h3>No matches found.</h3><% end %></div>
Listing 17: app\views\pizzas\search_all.html.erb
<%# Alle JavaScripte einbinden %>
<%= javascript_include_tag :defaults %>
<%# Suchfeld %>
<% form_tag({:action => 'search_all'}, :method => 'get') do %>
<label for='term'>Live-Search query:</label>
<%= text_field_tag :term %>
<% end %>
<%# Während der Live-Suche erscheint Grafik %>
<%= image_tag 'ajax-loader.gif', :id => 'loading', :style =>
'display:none' %>
<%# Observer überwacht das Suchfeld und führt action aus %>
<%= observe_field('term', :url => {:action => 'search_all'},
:with => 'term', :frequency => 1, :update => 'live_results',
:before => "Element.show('loading'); Element.hide('live_results')", :complete => "Element.hide('loading');" + visual_effect(:appear,:live_results ))
%>
Listing 18: app\views\pizzas\_live_search.html.erb
def search_all
[...]
#Rendert das Ergebnis
respond_to do |format|
if request.xhr? # AJAX Anfrage? -> Layout nicht neu rendern
render :layout => false and return
end
format.html # Rendert views\pizzas\search_all.html.erb
# Falls XML-Anfrage rendere XML-Antwort
format.xml { render :xml => @pizzas } # Rendert XML
end
end
Listing 19: Aktualisierte Methode in: app\controllers\pizzas_controller.rb
Das zentrale Element hierbei ist der Observer, der bei Änderungen im Suchfeld die Aktion search_all neu ausführt und den DIV-Container live_results aktualisiert.
4.5 E-Mail Versand durch ActionMailer
Damit die Bestellungen auch Beachtung finden, werden sie nach ihrem erfolgreichen
Anlegen mit Hilfe des ActionMailer-Moduls per E-Mail an den admin (bspw. thomas@jansing.de) versandt. Hierzu wird das Model order_mailer generiert:
“ruby script\generate mailer order_mailer order_email”
class OrderMailer < ActionMailer::Base # Erbt von ActionMailer
def order_email(order, sent_at = Time.now) # Order als Param.
19
Kapitel 4: Erweiterung des Projektes
@subject = 'New Pizza-Order'
@recipients = 'thomas@jansing.de'
@from = 'Pizza-Service'
@sent_on = sent_at
@body = {:order => order} # Order-Objekt wird übergeben
end
end
Listing 20: app\models\order_mailer.rb
respond_to do |format|
if @order.save
# Schicke Bestellung als Email
OrderMailer.deliver_order_email(@order)
[...]
Listing 21: Erweiterung der Create-Methode in: app\controllers\orders_controller.rb
Beim Ausführen der create-Methode in OrdersController wird die deliver_order_email-Methode von OrderMailer aufgerufen und ihr das Order-Objekt
übergeben. Durch die Variable @body steht es im Template des E-Mail-Body über
@order wieder zur Verfügung, so dass alle Werte der Bestellung eingetragen werden
können. Das Template views\orders\order_email.erb befindet sich im Anhang
A. Standardmäßig versendet ActionMailer E-Mails über einen SMTP-Server, dessen
Zugangsdaten in der Datei config\environment.rb hinterlegt werden müssen [Ruby08e]. Auch das Versenden über sendmail wird unterstützt. Zum Testen kann aber
auch ohne Konfiguration das Versenden der E-Mail im Server-Log überprüft werden.
4.6 Sicherheitsaspekte
Nach [Or07, Kap. 11] ist Sicherheit insbesondere bei Web-Anwendungen von großer
Bedeutung, da diese über das Internet stets öffentlich zugänglich sind. Da der öffentliche Zugang zu bestimmten Dateien auf dem Webserver zwingend notwendig ist, kann
nicht verhindert werden, dass potentielle Angreifer versuchen die Web-Anwendung auf
Schwachstellen und Sicherheitslücken zu untersuchen. Diese Attacken können auch
automatisiert von Skripten durchgeführt werden und versuchen dabei bekannte Sicherheitslücken auszunutzen. Die zwei größten sicherheitskritischen Bereiche sind einerseits
„SQL-Injection“ sowie „cross-site scripting“ (XSS). Dazu ergänzen [WB06, Kap. 11.1]
noch die für Ruby on Rails-Anwendungen kritischen Batch-Updates von Modellen,
mögliche unsichere Dateiendownloads durch die send_file() Methode und die Direkteingabe einer URL mit ID. Grundsätzlich sollten alle möglichen Benutzer-Eingaben
20
Kapitel 4: Erweiterung des Projektes
gefiltert und validiert werden und der Output auf unsichere und ungültige Zeichen und
oder Informationen überprüft werden („escape output“).
Zur Vermeidung von SQL-Injection sollte nach [WB06, S. 234] bei Datenbankabfragen
in der Geschäftslogik nicht der Ruby-Ersetzungsmechanismus „#{code}“ benutzt
werden, da dieser die Benutzereingaben ungeprüft in die SQL-Abfrage einsetzen würde:
Person.find(:first, :conditions => “user = ‘#{params[:user]}’ ”
+ “and pw = ‘#{params[:password]}’”)
Die Eingabe „'OR 1 --'“ als Passwort, ergibt die SQL-Abfrage:
SELECT * FROM persons where user = ‘anyone’ and pw = ‘'OR 1 --'’
Da der letzte Teil des Ausdrucks immer „wahr“ ergibt, liefert die Abfrage immer einen
Benutzer zurück, wodurch ein potentieller Angreifer Zugang erhält. Daher sollte entweder die Binding-Funktionalität von Active Record:
Person.find(:first, :condition => [“user=? and pw=?”, user, pw])
oder die automatisch generierten dynamischen Finder verwendet werden:
Person.find_by_name_and_pw(user,pw)
Zur Vermeidung von XSS empfiehlt [Or07, S. 369 f.] die konsequente Benutzung der
html_escape() Methode von Ruby für alle Variablen, die erst bei der Generierung
der Views ausgewertet werden. Dadurch werden alle potentiellen „tainted variables“
ausgewertet, bevor der HTML-View gerendert wird. Somit kann kein Skript eingeschleust werden. Daher sollten in allen *.html.erb Dateien (View-Vorlagen) die Ruby Variablen „<%= @einWert %>“ mit „<%= html_escape(@einWert) %>“ oder
kurz „<%=h @einWert %>“ umschlossen werden (Vgl. Kap. 4.3).
Wenn Instanzen von Modellen in Rails erzeugt oder geändert werden, bspw. bei der
Registrierung eine Instanz eines neuen User-Modells, sollte immer darauf geachtet werden, dass über den Parameter-Hash params nicht zusätzliche Daten übertragen werden,
die nicht explizit im View vorgesehen sind, wie z. B. ein admin-Attribut, welches nur
in der Logik gesetzt wird. Im entsprechenden User-Model können zu schützende Attribute durch „attr_protected :attribut“ gekennzeichnet werden. Diese werden
bei Batch-Updates wie new() oder create() nicht aktualisiert, sofern sie nicht explizit in der Geschäftslogik gesetzt wurden [WB06, S. 234 f.]. Einen guten Überblick aller
sicherheitsrelevanten Aspekte von Rails 2.0 bietet [We08].
21
Kapitel 5: Zusammenfassende Betrachtung
5 Zusammenfassende Betrachtung
Nachdem zuerst die Grundlagen von Ruby on Rails erläutert wurden, wurde im Hauptteil dieser Ausarbeitung gezeigt, wie einfach sich ein Prototyp einer Web-Anwendung
entwickeln lässt. Im Vergleich zu anderen Web-Frameworks fällt hierbei positiv auf,
dass das Paradigma „Konvention über Konfiguration“ viel Konfigurationsaufwand ersparte. Angefangen bei der Entwicklungsumgebung, die schnell aufgesetzt war, wurde
dieses insbesondere beim Prototyping sehr deutlich. Das ORM war sofort ohne Anpassungen nutzbar und es müssten nur die Assoziationen eingefügt werden. CRUDOperationen konnten sofort sowohl im HTML-View als auch als RESTful WebServices ausgeführt werden. Ein nicht zu unterschätzender Vorteil sind die Namenskonventionen, die dafür sorgen, dass Rails bspw. automatisch die richtigen Templates benutzt oder automatisch dynamische Methoden bereitstellt, wie z. B. die FinderMethoden (find_by_attribut). Die intuitive Syntax von Ruby erleichtert das Verstehen des Quellcodes enorm und sieht sehr aufgeräumt aus. Grundsätzlich haben Parameter sinnvolle Default-Werte, weswegen sie nicht angegeben/geändert werden müssen. Ruby on Rails ist ein sehr umfangreiches Framework geworden und hat speziell
durch die Version 2.0 viele sinnvolle Veränderungen erfahren. Die Validierung im Modell ist einfach und effektiv zugleich, ohne sich um die Darstellung zu sorgen. Nicht
unwichtig ist die gute AJAX-Einbindung oder der leichte E-Mail Versand. Wenn einige
sicherheitsrelevante Aspekte beachtet werden kann mit Ruby on Rails eine produktive
Umsetzung erfolgen. Als Kritikpunkt ist die fehlende eingebaute Lokalisierung anzusehen, die eine Internationalisierung schwierig gestaltet, da diese selbst implementiert
werden muss. Ab der Version 2.2 soll Rails eine einfache Lokalisierung enthalten. Außerdem fehlt es im Vergleich zu J2EE-Umgebungen dem Rails Framework sicherlich an
Unterstützung durch Hersteller und entsprechenden Erweiterungen. Eine Betrachtung
des Aspekts Performance überstieg den Umfang dieser Ausarbeitung. Allgemein kann
aber die Aussage getroffen werden, dass die Performance im Vergleich zu JavaUmgebungen schlechter ist, da Rails auf einer Interpreter-Sprache basiert. Die Performance ist aber vergleichbar oder sogar besser als bei anderen etablierten WebFrameworks wie Django (basiert auf Python) oder Symfony (basiert auf PHP). Für eine
ausführliche Betrachtung der Performance von Rails sei auf [OR07, Kap 12] verwiesen.
Problematisch sind jedoch die schlechte Performance von Rails mit einer Oracle DB
[Ruby08c], sowie die Inkompatibilität mit MySQL unter Windows XP (Vgl. Kap 2.4).
22
Anhang A: Quelltexte
A Quelltexte
Um die Umsetzung des Projektes zu dokumentieren, werden hier anhand der Reihenfolge der Ausarbeitung die Quelltexte zu den einzelnen Kapiteln angegeben. Es werden
nur veränderte Dateien gelistet, andere nicht gelistete Dateien blieben unverändert. Da
einzelne Dateien im Laufe der Ausarbeitung mehrfach verändert wurden, wird jeweils
die komplett neue Datei gelistet. Falls nur kleinere Änderungen an großen Dateien vorgenommen wurden wird der unveränderte Teil der Datei mit [...] abgekürzt.
Die kompletten Quelltexte und alle Dateien des Projektes sind in dem Archiv
RoR_Kapitel4.zip enthalten und stellen die letzte Version des Projekts am Ende des
4. Kapitels dar. Zum Testen der Quelltexte muss eine Entwicklungsumgebung, wie in
Kapitel 2.4 beschrieben, eingerichtet werden.
Kapitel 3.2:
class Pizza < ActiveRecord::Base
# Einer Pizza werden 0..* Bestellungen zugeordnet
has_and_belongs_to_many :order
end
Listing 22: app\models\pizza.rb
class Order < ActiveRecord::Base
# Einer Bestellung werden 0..* Pizzen zugeordnet
has_and_belongs_to_many :pizza
# Eine Bestellung wird genau einer Person zugeordnet
belongs_to :user
end
Listing 23: app\models\order.rb
class User < ActiveRecord::Base
# Einem User werden 0..* Bestellungen zugeordnet
has_many :order
end
Listing 24: app\models\user.rb
class CreateOrders < ActiveRecord::Migration
def self.up # Neues Schemata wird migriert
create_table :orders do |t|
t.text :delivery_wish
t.integer :user_id # Fremdschlüssel wird ergänzt
t.timestamps
end
# Hilfstabelle wird angelegt. Keine eigene id.
create_table :orders_pizzas, :id => false do |t|
t.integer :order_id # Fremdschlüssel der Bestellungen
t.integer :pizza_id # Fremdschlüssel der Pizzen
23
Anhang A: Quelltexte
end
end
def self.down # Alte Tabellen/Schemata werden gelöscht
drop_table :orders
drop_table :orders_pizzas # Hilfstabelle entfernen
end
end
Listing 25: db\migrate\timestamp_create_orders.rb
Kapitel 3.3:
pizza1 = Pizza.new(:name => "Pizza Margherita", :price => 4.95,
:ingredients => "Tomaten, Kaese")
pizza2 = Pizza.new(:name => "Pizza Salami", :price => 5.45,
:ingredients => "Tomaten, Kaese, Salami")
pizza3 = Pizza.new(:name => "Pizza Tono", :price => 5.95,
:ingredients => "Tomaten, Kaese, Thunfisch")
user1 = User.new(:name => "Max Mustermann", :address =>
"Hansaring 55", :zip_code => 48143, :city => "Muenster")
user2 = User.new(:name => "Peter Muster", :address =>
"Wolbecker Str. 55", :zip_code => 48143, :city => "Muenster")
pizza1.save
pizza2.save
pizza3.save
user1.save
user2.save
order1 = Order.new(:delivery_wish => "Bitte noch 1 Liter Cola")
user1.order << order1 # Bestellung wird User zugeordnet (1:m).
order1.pizza << pizza2 #.pizza ist ein Array, wg. n:m Beziehung
order1.pizza << pizza3
order1.save
bestellung = Order.find(:first) # Suche nach der 1. Bestellung
bestellung.user.name
# Gibt den Namen des Kunden aus
# Alle Pizzen werden ausgelesen und deren Namen angezeigt
bestellung.pizza.each { |pizza| puts pizza.name }
Listing 26: Alle Eingaben in der Ruby-Konsole
24
Anhang A: Quelltexte
class OrdersController < ApplicationController
# Über den params-Hash des Select-Feldes werden die IDs
# der selektierten Pizzen übergeben und der Bestellung
zugeordnet
def handle_pizzas_orders
if params['pizza_ids']
@order.pizza.clear
pizzas = params['pizza_ids'].map { |id|
Pizza.find_by_id(id) }
@order.pizza << pizzas
end
end
[...]
# GET /orders/new
# GET /orders/new.xml
def new
@order = Order.new
@pizzas = Pizza.find(:all).collect {|pizza| [pizza.name,
pizza.id]}
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @order }
end
end
[...]
# POST /orders
# POST /orders.xml
def create
@order = Order.new(params[:order])
# Weise die selektierten Pizzen der Bestellung zu
handle_pizzas_orders
respond_to do |format|
if @order.save
flash[:notice] = 'Order was successfully created.'
format.html { redirect_to(@order) }
format.xml { render :xml => @order, :status =>
:created, :location => @order }
else
format.html { render :action => "new" }
format.xml { render :xml => @order.errors, :status =>
:unprocessable_entity }
end
end
end
[...]
end
Listing 27: app\controllers\orders_controller.rb
<h1>Listing orders</h1>
25
Anhang A: Quelltexte
<table>
<tr>
<th>Ordering date/time</th>
<th>Orderer ID</th>
<th>Order Items</th>
<th>Delivery wish</th>
<th>Actions</th>
</tr>
<% for order in @orders %>
<tr>
<td><%=h order.created_at %></td>
<td><%=h order.user_id %></td> <%# order.user.name
funktioniert nur in der Console %>
<td><%=h order.pizza.collect { |pizza| pizza.name }.join(',
') %>
<td><%=h order.delivery_wish %></td>
<td><%= link_to 'Delete', order, :confirm => 'Are you
sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New order', new_order_path %>
Listing 28: app\views\orders\index.html.erb
<h1>New order</h1>
<% form_for(@order) do |f| %>
<%= f.error_messages %>
<p>Orderer:<br>
<%= select 'order', 'user_id', User.find(:all).collect
{|user| [user.name, user.id]} %>
</p>
<p>Pizzas:<br>
<select id="pizza_ids" name ="pizza_ids[]"
multiple="multiple">
<%= options_for_select @pizzas %>
</select>
</p>
<p>
<%= f.label :delivery_wish %><br />
<%= f.text_area :delivery_wish %>
</p>
<p>
<%= f.submit "Create" %>
</p>
<% end %>
<%= link_to 'Back', orders_path %>
Listing 29: app\views\orders\new.html.erb
26
Anhang A: Quelltexte
Kapitel 3.4:
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
# protect_from_forgery # :secret =>
'3f96918d481c430681793223d9275efb'
end
Listing 30: app\controllers\application.rb
class PizzasController < ApplicationController
# GET /pizzas/search_all
# GET /pizzas/search_all.xml
def search_all
#Suchbegriff wird für die SQL Abfrage mit % geklammert
@term = "%#{params[:term]}%"
#Suchanfrage wird ausgeführt. SQL-Injection wird vermieden.
@pizzas = Pizza.find(:all, :conditions =>
["ingredients like ?", @term ])
#Rendert das Ergebnis nur als XML, kein HTML, kein AJAX
respond_to do |format|
format.xml { render :xml => @pizzas }
end
end
[...]
Listing 31: app\controllers\pizzas_controller.rb
ActionController::Routing::Routes.draw do |map|
map.resources :users
map.resources :orders
# "search_all“ Anfrage mit „get“ request wird weitergeleitet
# „map.resources :pizzas“ wird geändert in:
map.resources :pizzas, :collection => {:search_all => :get}
[...]
Listing 32: config\routes.rb
Kapitel 4.1:
class LoginController < ApplicationController
def login
session[:user] = nil # User-Objekt und Werte löschen
session[:user_id] = nil
session[:user_login] = nil
# Login-Button löst POST-Request aus
if request.post?
# Authentifiziere User -> app\models\user.rb
user = User.authenticate(params[:login],
params[:password])
27
Anhang A: Quelltexte
# Konnte User-Objekt erfolgreich geladen werden?
if user != nil # Wenn ja setze neues Objekt und Werte
session[:user] = user
session[:user_id] = user.id
session[:user_login] = user.login
# Hinweis und Redirect
flash[:notice] = "Successfully logged in!"
redirect_to :controller => "pizzas", :action => "index"
else # Fehler bei der Authentifizierung
flash[:error] = "Login or passwort are incorrect!"
end
end
end
def logout
reset_session # Löscht alle Session Objekte des Users
end
end
Listing 33: app\controllers\login_controller.rb
class User < ActiveRecord::Base
# Einem User werden 0..* Bestellungen zugeordnet
has_many :order
# Authentifizierung (Passwort-Vergleich)
def self.authenticate(login, password)
user = self.find_by_login(login) # Lade User-Objekt
if user # User-Objekt erfolgreich geladen, prüfe Passwort
if user.password != password
user = nil
# Passwort falsch. Gib Fehler zurück (NIL)
end
end
user
# Passwort richtig. Gib User-Objekt zurück
end
end
Listing 34: app\models\user.rb
# Filters added to this controller apply to all controllers in
the application.
# Likewise, all the methods added will be available for all
controllers.
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
private
# Überprüft ob User eingeloggt ist
def logged_in
if session[:user] == nil
flash[:error] = "You need to login first!"
redirect_to(:action => 'index')
end
28
Anhang A: Quelltexte
end
private
# Überprüft ob User sein eigenes Profil editiert
def own_profile
if session[:user].id.to_s != params[:id]
flash[:error] = "You can only edit your own profile."
redirect_to(:action => 'index')
end
end
private
def admin_logged_in
# Überprüft ob user.login = admin
logged_in
if session[:user] != nil
if session[:user].login.to_s == "admin"
flash.now[:notice] = "You have admin rights."
else
flash[:error] = "You need to have admin rights to use
this function."
redirect_to(:action => 'index')
end
end
end
protect_from_forgery # :secret =>
'3f96918d481c430681793223d9275efb'
end
Listing 35: app\controllers\application.rb
class OrdersController < ApplicationController
# Delete-/Edit-Aktionen sind admin-geschützt, Anzeigen und
Anlegen nicht
before_filter :admin_logged_in, :only => [:delete, :destroy,
:edit, :update]
# Um Bestellungen anzulegen muss User eingeloggt sein
before_filter :logged_in, :only => [:create, :new]
[...]
Listing 36: app\controllers\orders_controller.rb
class PizzasController < ApplicationController
#Create/Update/Delete-Aktionen sind geschützt (nur admin).
Anzeigen und Suchen nicht.
before_filter :admin_logged_in, :except => [:index, :list,
:show, :search_all]
[...]
Listing 37: app\controllers\pizzas_controller.rb
class UsersController < ApplicationController
# Delete-Aktionen sind admin-geschützt, Anzeigen und Anlegen
29
Anhang A: Quelltexte
nicht
before_filter :admin_logged_in, :only => [:delete, :destroy]
# User dürfen nur ihre eigenen Profile ändern
before_filter :logged_in, :own_profile, :only => [:edit,
:update]
[...]
Listing 38: app\controllers\users_controller.rb
<h1>Login</h1>
<% form_tag do %>
<p>
<label for="name">Login: </label>
<%= text_field_tag :login, params[:login] %>
</p>
<p>
<label for="password">Password:</label>
<%= password_field_tag :password, params[:password] %>
</p>
<p>
<%= submit_tag "Login" %>
</p>
<% end %>
<p>
<%= link_to 'Create new account', :controller => 'users',
:action => 'new' %>
</p>
Listing 39: app\views\login\login.html.erb
<h1>Logout</h1>
<p>Logged out!</p>
Listing 40: app\views\login\logout.html.erb
<h1>New order</h1>
<% form_for(@order) do |f| %>
<%= f.error_messages %>
<p>Orderer<br><strong>
<% if session[:user] == nil %>
<div style="display:none"><%= select 'order', 'user_id',
'0', :disabled => true %></div>
<% else %> <%# Zeige User-Adresse zur Kontrolle %>
<div style="display:none"><%= select 'order', 'user_id',
session[:user].id.to_s, :disabled => true %></div>
<%=h session[:user].name %><br>
<%=h session[:user].address %><br>
<%=h session[:user].zip_code %>
<%=h session[:user].city %>
<% end %></strong>
</p>
<p>Pizzas<br>
30
Anhang A: Quelltexte
<select id="pizza_ids" name ="pizza_ids[]"
multiple="multiple">
<%= options_for_select @pizzas %>
</select>
</p>
<p>
<%= f.label :delivery_wish %><br />
<%= f.text_area :delivery_wish %>
</p>
<p>
<%= f.submit "Create" %>
</p>
<% end %>
Listing 41: app\views\orders\new.html.erb
Kapitel 4.2:
class Pizza < ActiveRecord::Base
# Einer Pizza werden 0..* Bestellungen zugeordnet
has_and_belongs_to_many :order
# Alle drei Attribute müssen angegeben werden
validates_presence_of :name, :price, :ingredients
# Das Attribut “Name” der Pizza soll eindeutig sein
validates_uniqueness_of :name
# Das Attribut “Preis” soll positiv sein
validates_numericality_of :price, :on => :create,
:greater_than => 0, :message => "Price must be > 0."
end
Listing 42: app\models\pizza.rb
class Order < ActiveRecord::Base
# Einer Bestellung werden 0..* Pizzen zugeordnet
has_and_belongs_to_many :pizza
# Eine Bestellung wird genau einer Person zugeordnet
belongs_to :user
# Eine Bestellung muss min, 1 Pizza enthalten
validates_presence_of :user, :pizza
end
Listing 43: app\models\order.rb
class User < ActiveRecord::Base
# Einem User werden 0..* Bestellungen zugeordnet
has_many :order
# Minimale/Maximale Länge kann vorgegeben werden
validates_length_of :login, :within => 5..30
validates_length_of :password, :within => 6..30
# Alle Attribute müssen angegeben werden
validates_presence_of :login, :password, :name, :address,
31
Anhang A: Quelltexte
:zip_code, :city
# Wichtige Überprüfung, damit login "admin" einmalig bleibt
validates_uniqueness_of :login, :on => :create
validates_confirmation_of :password, :on => :create
# Authentifizierung (Passwort-Vergleich)
def self.authenticate(login, password)
user = self.find_by_login(login) # Lade User-Objekt
if user # User-Objekt erfolgreich geladen, prüfe Passwort
if user.password != password
user = nil
# Passwort falsch. Gib Fehler zurück (NIL)
end
end
user
# Passwort richtig. Gib User-Objekt zurück
end
end
Listing 44: app\models\user.rb
Kapitel 4.3:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en">
<head>
<meta http-equiv="content-type"
content="text/html;charset=UTF-8" />
<title>Pizza-Service: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'pizza' %>
</head>
<body>
<fieldset><legend>Options</legend><strong>
<%= link_to 'Pizzas', :controller => 'pizzas', :action =>
'index' %> |
<%= link_to 'Orders', :controller => 'orders', :action =>
'index' %> |
<%= link_to 'Users', :controller => 'users', :action =>
'index' %><br>
<% if session[:user_login] == nil %><%= link_to 'Login',
:controller => 'login', :action => 'login' %>
<% else %><%= link_to 'Logout', :controller => 'login',
:action => 'logout' %><% end %> |
<% if session[:user_login] != nil %><%= link_to 'Profile',
:controller => 'users', :action => 'edit', :id =>
session[:user_id] %>
<% else %><%= link_to 'Register', :controller => 'users',
:action => 'new' %><% end %></strong>
<br>Status: <% if session[:user_login] != nil %><span
style="color: green">Logged in [<%= session[:user_login]
%>]</span><% else %><span style="color: red">Not logged in<%
end %></span>
</fieldset>
32
Anhang A: Quelltexte
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: red"><%= flash[:error] %></p>
<fieldset><legend>Content</legend>
<%= yield %>
</fieldset>
<p style="font-size: 10px">Pizza-Demo powered by Ruby on Rails
| Thomas Jansing</p>
<img src="/images/rails.png" border="0">
</body>
</html>
Listing 45: app\views\layouts\application.html.erb
body { background-color: #EEE; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size:
13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
table {
border: 1px solid;
padding: 1px;
margin: 1px;
background-color: #DDD;
}
td{
}
border: 1px solid;
padding: 1px;
margin: 1px;
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
33
Anhang A: Quelltexte
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
Listing 46: public\stylesheets\pizza.css
Zusätzlich wurde die Datei public\images\ajax-loader.gif hinzugefügt. Diese
befindet sich im Archiv RoR_Kapitel4.zip.
Kapitel 4.4:
<%# Die AJAX-Suche wird als Partial-View eingebunden %>
<%= render :layout => false, :partial => 'live_search' unless
request.xhr? %>
<%# Bei Ergebnisse erzeuge Tabelle%>
<div id="live_results">
<% unless @pizzas.empty? %>
<h3>Search results</h3>
<table width="500" >
<tr><th width="200">Name</th>
<th width="100">Price</th>
<th width="200">Ingredients</th></tr>
<% for pizza in @pizzas %>
<tr><td><%=h pizza.name %></td>
<td><%=h pizza.price %></td>
<td><%=h pizza.ingredients %></td></tr>
<% end %>
</table>
<% else %><%# Wenn keine Ergebnisse vorliegen %>
<h3>No matches found.</h3>
<% end %></div>
Listing 47: app\views\pizzas\search_all.html.erb
34
Anhang A: Quelltexte
<%# Alle JavaScripte einbinden %>
<%= javascript_include_tag :defaults %>
<%# Suchfeld %>
<% form_tag({:action => 'search_all'}, :method => 'get') do %>
<label for='term'>Live-Search query:</label>
<%= text_field_tag :term %>
<% end %>
<%# Während der Live-Suche erscheint Grafik %>
<%= image_tag 'ajax-loader.gif', :id => 'loading', :style =>
'display:none' %>
<%# Observer überwacht das Suchfeld und führt action aus %>
<%= observe_field('term', :url => {:action => 'search_all'},
:with => 'term', :frequency => 1, :update => 'live_results',
:before => "Element.show('loading');
Element.hide('live_results')", :complete =>
"Element.hide('loading');" +
visual_effect(:appear,:live_results )) %>
Listing 48: app\views\pizzas\_live_search.html.erb
class PizzasController < ApplicationController
#Create/Update/Delete-Aktionen sind geschützt (nur admin).
Anzeigen und Suchen nicht.
before_filter :admin_logged_in, :except => [:index, :list,
:show, :search_all]
# GET /pizzas/search_all
# GET /pizzas/search_all.xml
def search_all
#Suchbegriff wird für die SQL Abfrage mit % geklammert
@term = "%#{params[:term]}%"
#Suchanfrage wird ausgeführt. SQL-Injection wird vermieden.
@pizzas = Pizza.find(:all, :conditions =>
["ingredients like ?", @term ])
#Rendert das Ergebnis
respond_to do |format|
if request.xhr? # AJAX Anfrage? -> Layout nicht neu
rendern
render :layout => false and return
end
format.html # Rendert views\pizzas\search_all.html.erb
# Falls XML-Anfrage rendere XML-Antwort
format.xml { render :xml => @pizzas } # Rendert XML
end
end
[...]
Listing 49: app\controllers\pizzas_controller.rb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en">
35
Anhang A: Quelltexte
<head>
<meta http-equiv="content-type"
content="text/html;charset=UTF-8" />
<title>Pizza-Service: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'pizza' %>
</head>
<body>
<fieldset><strong>
<%= link_to 'Pizzas', :controller => 'pizzas', :action =>
'index' %> |
<%= link_to 'Orders', :controller => 'orders', :action =>
'index' %> |
<%= link_to 'Users', :controller => 'users', :action =>
'index' %><br>
<% if session[:user_login] == nil %><%= link_to 'Login',
:controller => 'login', :action => 'login' %>
<% else %><%= link_to 'Logout', :controller => 'login',
:action => 'logout' %><% end %> |
<% if session[:user_login] != nil %><%= link_to 'Profile',
:controller => 'users', :action => 'edit', :id =>
session[:user_id] %>
<% else %><%= link_to 'Register', :controller => 'users',
:action => 'new' %><% end %></strong>
<br>Status: <% if session[:user_login] != nil %><span
style="color: green">Logged in [<%= session[:user_login]
%>]</span><% else %><span style="color: red">Not logged in<%
end %></span><br>
<%= link_to 'Search_Pizzas_AJAX', :controller => 'pizzas',
:action => 'search_all' %> |
<%= link_to 'Search_Pizzas_XML', :controller => 'pizzas',
:action => 'search_all.xml' %><br>
</fieldset>
<p style="color: green"><%= flash[:notice] %></p>
<p style="color: red"><%= flash[:error] %></p>
<fieldset>
<%= yield
</fieldset>
%>
<p style="font-size: 10px">Pizza-Demo powered by Ruby on Rails
| Thomas Jansing</p>
<img src="/images/rails.png" border="0">
</body>
</html>
Listing 50: app\views\layouts\application.html.erb
Kapitel 4.5:
class OrderMailer < ActionMailer::Base # Erbt von ActionMailer
def order_email(order, sent_at = Time.now) # Order als Param.
@subject = 'New Pizza-Order'
@recipients = 'thomas@jansing.de'
@from = 'Pizza-Service'
36
Anhang A: Quelltexte
@sent_on = sent_at
@body = {:order => order} # Order-Objekt wird übergeben
end
end
Listing 51: app\models\order_mailer.rb
class OrdersController < ApplicationController
[...]
# POST /orders
# POST /orders.xml
def create
@order = Order.new(params[:order])
# Weise die selektierten Pizzen der Bestellung zu
handle_pizzas_orders
respond_to do |format|
if @order.save
OrderMailer.deliver_order_email(@order)
flash[:notice] = 'Order was successfully created.'
format.html { redirect_to(@order) }
format.xml { render :xml => @order, :status =>
:created, :location => @order }
else
format.html { render :action => "new" }
format.xml { render :xml => @order.errors, :status =>
:unprocessable_entity }
end
end
end
[...]
Listing 52: app\controllers\orders_controller.rb
Pizza Order:
Orderer: <%= @order.user.name %>
Address: <%= @order.user.address %>
Zip_Code: <%= @order.user.zip_code %>
City: <%= @order.user.city %>
Pizzas: <%= @order.pizza.collect { |pizza| pizza.name }.join(',
') %>
Delivery-Wish: <%= @order.delivery_wish %>
Ordering date/time: <%= @order.created_at %>
Listing 53: app\views\order_mailer\order_email.erb
37
Literaturverzeichnis
[Ca07]
Denny Carl: Praxiswissen Ruby on Rails, O’Reilly, 2007.
[CR06]
Lucas Carlson, Leonard Richardson: Ruby Cookbook, O’Reilly, 2006.
[Fi00]
Roy Thomas Fielding: Architectural Styles and the Design of Networkbased Software Architectures, Dissertation, 2000,
[http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm], Abrufdatum:
05.11.2008.
[Ga07]
Johannes Gamperl: AJAX – Grundlagen, Frameworks, APIs, Galileo Computing, 2. Auflage, 2007.
[MO08]
Hussein Morsy, Tanja Otto: Ruby on Rails 2 - Das Entwickler-Handbuch,
Galileo Computing <openbook>,
[http://openbook.galileocomputing.de/ruby_on_rails/], Abrufdatum:
04.11.2008.
[MR06]
Martin Marinschek, Wolfgang Radinger: Ruby on Rails, Einstieg in die effiziente Webentwicklung, dpunkt.verlag, 2006.
[Nu08]
Nubyonrails.com: Pluralization Tester for the Ruby on Rails Inflector,
[http://nubyonrails.com/tools/pluralize], Abrufdatum: 08.11.2008.
[Op08]
Open Source Initiative OSI: The MIT License,
[http://www.opensource.org/licenses/mit-license.php], Abrufdatum:
01.11.2008.
[Or07]
Rob Orsini: Rails Cookbook, O’Reilly, 2007.
[RR07]
Leonard Richardson, Sam Ruby: Web-Services mit REST - Frischer Wind
für Web-Services durch REST, O’Reilly, 2007.
[Ruby08a] RubyForge: One-Click Ruby Installer: File Release Notes and Changelog,
[http://rubyforge.org/frs/shownotes.php?release_id=26150], Abrufdatum:
03.11.2008.
[Ruby08b] Official Ruby on Rails Wiki: Database Drivers,
[http://wiki.rubyonrails.com/rails/pages/DatabaseDrivers], Abrufdatum:
03.11.2008.
[Ruby08c] Official Ruby on Rails Wiki: Framework Performance,
[http://wiki.rubyonrails.org/rails/pages/Framework+Performance], Abrufdatum: 08.11.2008.
[Ruby08d] Ruby on Rails Security Project: CSRF - An underestimated attack method,
[http://www.rorsecurity.info/2008/05/05/csrf-an-underestimated-attackmethod/], Abrufdatum: 10.11.2008.
[Ruby08e] Official Ruby on Rails Wiki: How To Send Emails With Action Mailer,
[http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMaile
r], Abrufdatum: 05.12.2008.
[Sqlite08] SQLite Download Page, [http://www.sqlite.org/download.html], Abrufdatum: 10.11.2008.
[St08]
Bettina Stracke: Web 2.0 mit Ruby on Rails – Professionelle Anwendungen
mit Ajax, Mashups und dem Rails Framework, entwickler.press, 2008.
[TH04]
Dave Thomas, Andy Hunt: Der Pragmatische Programmierer, Carl Hanser
Verlag, 2004.
[Vo07]
Deepak Vohra: Ruby on Rails for PHP and Java Developers, SpringerVerlag, 2007.
[Wa08]
Thomas Walter: Kompendium der Web-Programmierung - Dynamische
Web-Sites, Springer-Verlag, 2008.
[WB06]
Ralf Wirdemann, Thomas Baustert: Rapid Web Development mit Ruby on
Rails, Carl Hanser Verlag, 2006.
[We08]
Heiko Webers: Ruby on Rails Security - Version 2,
[http://www.rorsecurity.info/storage/rails_security_2.pdf], Abrufdatum:
11.11.2008.
[WK07]
Ramon Wartala, Jan Krutisch: Webanwendungen mit Ruby on Rails – Der
Praxiseinstieg von den Grundlagen über Testing bis Erweiterung, AddisonWesley Verlag, 2007.
[Xu08]
Xucia Incorporation: RESTTest, [http://www.xucia.com/#RestTest], Abrufdatum: 06.11.2008.