Ruby on Rails: Schedule

Unsere Applikation, eine online Kursverwaltung "Schedule" Bearbeiten

Contentmanagement und Terminverwaltung. Besonderheit: geschachtelte Termine (Beispiel: Ein Kurs der aus mehreren Kurs-Blöcken besteht, die an Wochenenden stattfinden). Später binden wir auch das OnlineGlossar ein.

Die Applikation anlegen Bearbeiten

Wie man eine Rails Applikation anlegt wissen wir schon aus dem ersten Kapitel. In diesem Fall soll die Applikation "schedule" heißen.

$ rails new schedule

Das "rails"-Kommando legt das Grundgerüst für eine Applikation an. In Rails arbeiten wir häufig mit Code-Generatoren. Es gibt in Rails sowohl Quelltext-Codegeneration als auch Laufzeit-Codegeneration. In diesem Fall wird Quelltext erzeugt. Rails erspart es uns den Code mit Kopieren und Einfügen aus einem andern Projekt herauszuholen und sorgt für eine saubere Strukturierung des Projekts.

  • Es ist vorgesehen, dass wir diese Struktur als Ausgangspunkt nehmen und das Projekt erweitern.
  • Es ist nicht vorgesehen, dass wir diesen Generator noch mal laufen lassen, wenn wir das Projekt schon ergänzt haben.

Exkurs: Rails Application Templates Bearbeiten

Seit Rails 2.3 gibt es die Möglichkeit diesem Projektgenerator eine Projektvorlage (Rails Application Template) mitzugeben. Rails erzeugt dann eine modifizierte Bassistruktur und bindet beispielweise bestimmte Softwarebibliotheken in das Projekt ein.

  • Syntax: rails app_name -m template
  • Beispiel: rails schedule -m ~/cms_template.rb

Es kann auch eine url für das Template übergeben werden

Vorlagen:

Wir können solche Vorlagen auch selbst erstellen. Manuell oder mit einem weiteren Generator. Ist das nicht toll. Ein Generator der Vorlagen für einen anderen Generator generiert.

Im Moment gehen wir darauf nicht weiter ein. Wir benutzen Rails erst einmal so, wie es konzipiert wurde. Zusätzliche Resourcen binden wir erst ein, wenn wir sie benötigen. Und erläutern sie dann.

Die Datenbank Bearbeiten

Zunächst brauchen wir eine Datenstruktur für unsere Kurse oder Kursblöcke. Machen wir uns klar welche Felder wir unbedingt brauchen und deshalb gleich anlegen welche vielleicht später dazukommen:

  • Titel — Jeder Kurs und jeder Kursblock sollte einen Titel haben: Datentyp String.
  • Beschreibung — Zu einem Kurs sollte man eine Beschreibung anlegen können. Eventuell wollen wir später eine lange und eine kurze Beschreibung, aber für's erste reicht ein Feld. Datentyp: Text, denn die Beschreibung kann lang sein.
  • Schlagworte — Zu jedem Kurs wollen wir Schlagworte ablegen. Das gibt später ein extra Modell. Für's erste können wir darauf verzichten.
  • Trainer — Eventuell kann ein Kurs mehrere Trainer haben. Auch das gibt später ein extra Modell. Fürs erste benutzen wir die Beschreibung.
  • Ort — An einen Veranstaltungsort können mehrere Kurse (nacheinander) stattfinden. Eventuell kann ein Kurs, der aus mehreren Blöcken besteht, auch an mehreren Orten stattfinden. Jeder Block woanders. Extra Modell, später.
  • Preis — Jeder buchbare Kurs sollte einen Preis haben. Kursblöcke, die nicht einzeln gebucht werden können brauchen keine Preisangabe. Dann bleibt das Feld leer. Für's erste ein Feld: Integer. Eventuell modellieren wir später mit Money-Objekten.
  • Beginn und Ende — Jeder Kurs und jeder Kursblock hat ein Anfangs und ein Enddatum. Für's erste arbeiten pflegen wir nur Tageskurse. Wir brauche also keine Uhrzeit, sondern nur das Datum. Datentyp: Date. (Termin)
  • Dauer — Die Kursdauer können wir aus den Anfangs und Endterminen der Kursblöcke berechnen. Virtuelles Attribut, kein Datenfeld.
  • Farbe — Wir wollen die Kurse in einer Kalenderansicht in verschiedenen Farben darstellen. Aber das kommt später und vielleicht ordnen wir die Farben nicht manuell zu sondern vergeben sie automatisch.
  • Zusammensetzung — Jeder Kurs oder Kursblock kann sich aus kürzeren Kursblöcken zusammensetzen. Umgekehrt kann jeder Kurs oder Kursblock zu einem größeren Kurs gehören. Fürs erste gehen wir davon aus dass es keine Kursblöcke gibt, die zu mehreren Kursen gehören. Dann haben wir eine Baumstruktur, die wir mit einer 1 zu n Beziehung modellieren. Datenfeld: ParentId, Datentyp: Integer.

Wir starten mit den Feldern, die wir jetzt brauchen. Weil wir auch zusammengesetzte Kurse haben, gehört dazu das zusätzliche Feld ParentID. Damit können wir eine Baumstruktur modellieren. Als Modellklasse für Kurse und Kursblöcke eine einzige. Nennen wir sie "Event".

Exkurs: Wenn wir für zusammengesetzte Konten und Endknoten unterschiedliche Klassen verwenden wollen, können wir mit STI (Single table Inheritance) auch das mit einer einzigen Datenbanktabelle modellieren. Wir benötigen nur ein weiteres Feld für den Namen der Klasse ("Datenfeld: Type, Datentyp: String").


TODO:

  • Wir erinnern uns: Scaffold generator
  • Alternativen: nur Modell generieren, oder alles Manuell
  • Entscheidung: Scaffold, weil Adminarea nötig

Aufruf:

script/generate scaffold event title:string description:text start:date end:date price:integer parent_id:integer

Mehr über script/generate (oder extra Kapitel + Verweis)

Ergebnis: Modell, Adminseiten, Test + Migration

Migrationen sind der Rails-Weg um Tabellen in der Datenbank anzulegen oder zu ändern. Sie tragen Versionsnumern bzw. Zeitstempel und wir erklären sie ausführlich im nächsten Kapitel: Datenbank ändern (Migrationen).

Hier schauen wir nur kurz rein ..

# ...create_events.rb

class CreateEvents < ActiveRecord::Migration
  def self.up
    create_table :events do |t|
      t.string :title
      t.text :description
      t.date :start
      t.date :end
      t.integer :price
      t.integer :parent_id

      t.timestamps
    end
  end

  def self.down
    drop_table :events
  end
end

Todo - timestamps

.. begrenzen wir den Titel auf sagen wir 32 Buchstaben und machen ihn zu einem Pflichtfeld:

  ...
  t.string :title, :limit => 32, :null => false
  ...

Als Datenbank verwenden wir wieder das vorkonfigurierte SQLite. Deshalb können wir die Migration direkt ausführen:

rake db:migrate

Das Eingabeformular Bearbeiten

Parallel ergänzen wir die Validierungen im Modell. Auch das kennen wir aus der Einführung. Und auch dazu gibt es gleich ein extra Kapitel: (TODO ..)

class Event < ActiveRecord::Base
  validates_presence_of :title
  validates_length_of :title, :maximum => 32
end

Jetzt können wir die Applikation ausprobieren und stellen fest:

  • Events lassen sich anlegen
  • Validierungen funktionieren
  • Layout fehlt noch
  • Keine Info was bei Preis rein soll
  • Ausfüllen von Start und Ende macht bei Sammelevents keinen Sinn. Besser wäre ein Kästchen "ergibt sich aus ..".
  • Ausfüllen der Parentid bei zusammensetzungen ist praktisch ummöglich
  • Feldbezeichnungen wie in der Datenbank, typischerweie englisch.

Damit sind wir beim Hauptthema dieses Kapitels. Formulare.

Schauen wir mal in new.html.erb was Rails generiert hat. "new.html.erb" ist die Formularseite für das neu anlegen einer Ressource. Die Endung "erb" steht für embedded ruby und genau das sehen wir: Html mit eingebettetem Ruby Code. Die Ruby-Bereiche sind mit "<% .. %>" geklammert. Für Ausgaben gibt es <%= .. %>

 # *.html.erb
 ..
 <h1>Html mit eingebettetem Ruby-Code</h1>
 <% 
 # Berechnung mit Ruby
 wurzel_zwei = Math.sqrt(2)
 %>
 <p>Die Quadratwurzel von 2 beträgt <%= wurzel_zwei %>.</p>
 ..

Dazu stellt Rails eine Reihe von HTML-Helper-Methoden zur Verfügung. So finden wir beispielsweise die Form-Helper-Methode "form_for" mit der das Event-Formular erzeugt wird.

<% form_for(@event) do |f| %>
  ..
<% end %>

Und Helper für die Formularfelder. Beispielsweise

    <%= f.text_field :title %>

für das Texfeld, in das der (Event-)Titel eingegeben wird.

Form-Helper Bearbeiten

Es gibt zwei Form-Helper in Rails. Der form_tag-Form-Helper, der einfach ein Formular erzeugt:

<% form_tag do %> 
  .. 
<% end %>

Und der form_for Form-Helper, den wir gerade gesehen haben und der das Formular zusätzlich an ein Ojekt bindet. Das hat den Vorteil, dass wir bei den Helpern für die Formularfelder das Objekt nicht mehr angeben müssen. Es ist ja bereits bekannt. Die Syntax für ein Textfeld ist dann wie gesehen:

  <%= f.text_field :title %>

statt

  <%= text_field :event, :title %>

was wir unter form_tag .. schreiben müssten.

TODO text_field_tag

Es gibt Formfield-Helper für:

Beschreibungen

  • label

Textfelder und Textbereiche (Area)

  • text_field
  • password_filed
  • hidden_field
  • text_area

Auswahlfelder

  • check_box
  • radio_button
  • select

Und Sende-Buttons

  • submit

Auswahlfeld Bearbeiten

Damit wir die Parent_ID setzen können, ersetzen wir das Textfeld mit einem Select-Auswahlfeld.

  <p>
    Vaterevent: <br />
    <%= f.select :parent_id, @available_parents %>
  </p>

statt

  <p>
    <%= f.label :parent_id %><br />
    <%= f.text_field :parent_id %>
  </p>

Der erste Parameter ist die Variable, die gesetzt werden soll, der zweite Parameter ist ein Feld mit den möglichen Optionswerten. Wir setzen sie im Controller:

  def new
    @event = Event.new
    
    available_events = Event.find(:all)
    @available_parents = available_events.map {|ev| [ev.title, ev.id] }
    
    ..
  end

(TODO collect statt map? ganz in den view schieben? <%= f.select :parent_id, Event.all.collect {|evt| [ evt.title, evt.id ] }, {:include_blank => 'kein Vaterevent'} %> oder tabelle im Model vorberechnen?)

Das sieht leider nur auf den ersten Blick gut aus, denn jetzt werden wir gezwungen ein Vaterelement auszuwählen. Glücklicherweise hat der Select-Formfield-Helper noch den optionalen Parameter :include_blank den wir benutzen können, wenn ein Termin kein Vaterelement haben soll. Damit erzeugen einen zusätzliche Optionswert. Den Text übergeben wir als Hashvalue.

  <p>
    Vaterevent: <br />
    <%= f.select :parent_id, @available_parents, {:include_blank => 'kein Vaterevent'} %>
  </p>

Edit-Formular Bearbeiten

Das Gleiche müssen wir natürlich auch beim Edit-Formular machen. Hier werfen wir zusätzlich den aktuellen Termin aus der Optionstabelle, weil ein Termin natürlich nicht sein eigener Vater sein darf.

# events_controller.rb
  
  def edit
    @event = Event.find(params[:id])
    
    available_events = Event.find(:all)
    available_events = available_events.reject { |ev| ev == @event }
    @available_parents = available_events.map {|ev| [ev.title, ev.id] }
  end


# views/events/edit.html.erb
  ..
  <p>
    Vaterevent <br />
    <%= f.select :parent_id, @available_parents, {:include_blank => 'kein Vaterevent'}  %>
  </p>
  ..

Übersichtsseite Bearbeiten

Außerdem wollen wir die Übersichtsseite richten.

Zunächst machen wir die Tabelle übersichtlich. Dazu benutzen wir den cycle-helper, den wir aus der Einführung kennen.

# views/events/index.html.erb
  ..
  <tr style="background-color: <%= cycle 'silver', 'white' %>;">
  ..

Dann kürzen wir die Beschreibung. Das machen wir diesmal direkt im View. Dort steht uns die helper-Methode truncate zur Verfügung. Wir übergeben auf wieviele Buchstaben wir den String kürzen wollen.

# views/events/index.html.erb
  ..
    <td><%=h truncate event.description, 32 %></td>
  ..

Und schließlich das wichtigste. Die Anzeige des Vaterelements in der Übersicht. Dazu erweitern wir die Eventklasse um eine Zugriffsfunktion auf den Titel des Vaterelements und zeigen ihn an.

# event.rb
  
  def parent_title
    self.parent_id ? Event.find(self.parent_id).title : " &mdash; "  # TODO fix &mdash
  end
# views/events/index.html.erb
  ..
    <td><%=h event.parent_title %></td>
  ..

Damit können wir unsere Termine administrieren. Allerdings haben wir vielleicht ein schlechtes Gewissen. Wir haben nämlich ganz ohne Tests entwickelt. Vielleicht ist es eine gute Idee jetzt ein paar Tests nachzuholen.

Tests Bearbeiten

Nun ganz so einfach ist es nicht. Wir haben jetzt einen ersten Einblick in Rails bekommen. Wir wissen was Migrationen sind, haben Modell, View und Controller gesehen und wissen, wie wir einfache Formulare erzeugen. Für Controller und Integrationstests reicht das noch nicht. Also kein schlechtes Gewissen. Testgetriebene Entwicklung ist noch ein frommer Wunsch. Arbeiten wir daran, ihn bald zu erfüllen. Und tun, was jetzt Sinn macht.

Aber was macht jetzt Sinn? Die Basisfunktionen von Rails, die wir verwendet haben, brauchen wir nicht zu testen, denn Rails ist ja getestet. Bleiben die Modellerweiterungen. Die können wir wir jetzt schon vernünftig testen. Und vielleicht gibt es ja noch ein paar andere Kleinigkeiten, die wir testen können ..

Modellerweiterungen zum Testen:

  • nur eine: event.parent_title().

Uns interessiert

  • das Verhalten, wenn noch kein event in der db ist (aktuelles event wurde mit new erzeugt)
  • das Verhalten, wenn nur das aktuelle event in der db ist (create)
  • das Verhalten, wenn es mehrere events in der db gibt und das aktuelle event keinen Vater hat
  • das Verhalten, wenn es mehrere events in der db gibt und das aktuelle event eines davon als Vater hat

Außerdem fällt auf, dass wir eine Eventzyklus bauen können

  • Modell 1 ist Vater von Modell 2 und
  • Modell 2 ist Vater von Modell 1.

Test dazu und per (handmade) validation verbieten.

Zunächst lassen wir alle Tests, die Rails für uns angelegt hat, laufen.

rake test

Uups. schon 2 Fehler. Allerdings keiner im Modell

rake test:units

Schauen wir mal, ob wir die Tests richten können, und ergänzen wir Tests für event.parent_title

TODO..

Weiteres Vorgehen Bearbeiten

  • content für cms
  • Seiten
  • Darstellung