Ruby on Rails: CMS
Bevor wir zur Darstellung kommen, wollen wir noch Texte und Seiten anlegen und verwalten können. Die Seiten können dann Texte oder Terminübersichten enthalten.
Texte administrieren für CMS
[Bearbeiten]Für Texte reicht uns zunächst ein einfacher Textcontainer ohne Struktur.
script/generate scaffold text content:text
rake db:migrate
Fertig. Oder auch nicht. Die Textübersichtsseite meldet: "undefined method `all' for Text:Module". Offensichtlich gibt es einen Namenskonflikt zwischen unserer Text-Klasse und dem vorhandenen Modul Text. Müssen wir nicht klären, wir können auf die Bezeichnung Textblock ausweichen.
Migration rückwärts
rake db:migrate:down VERSION=20090714210547
Alle erzeugten Dateien löschen. Dabei auch die Tests nicht vergessen.
Und dann neu:
script/generate scaffold textblock content:text
rake db:migrate
Jetzt klappts. Wir legen ein paar Textblöcke mit Blindtexten wie "Lorem ipsum .." an. Dann vielleicht noch die Darstellung auf der Übersichtsseite kürzen ... Kennen wir ja.
# app/views/textblocks/index.html.erb
<tr style="background-color: <%= cycle 'silver', 'white' %>;">
<td><%=h truncate textblock.content, :length => 64 %></td>
CMS-Seiten administrieren
[Bearbeiten]Die Seiten sind etwas komplexer. Wir wollen eine Menuebaum bauen. Dazu müssen die Seiten eine parent_id bekommen wie vorhin die events.
- parent_id:integer
Außerdem sollen die Seiten folgende Felder haben
- menutext:string
- title:string
- keywords:text
- description:text
also
script/generate scaffold page parent_id:integer menutext:string title:string keywords:text description:text
Wegen der Verschachtelung könnten wir den Code von den events übernehmen. Aber ist das dry? und ist er überhaupt gut?
Ersetzen durch plugin "acts_as_tree"
TODO: Gems, Plugins und acts_as_tree war Bestandteil von Rails. Ausgelagert in Bibliothek um Rails schlank zu halten.
Installation als Plugin:
script/plugin install git://github.com/rails/acts_as_tree.git
restart server
Änderungen im Model:
class Page < ActiveRecord::Base
acts_as_tree
end
View (Übersichts-, New- und Edit-Seite)
# ../index.html.erb
<td><%=h page.parent ? page.parent.menutext : ' - ' %></td>
# ../new.html.erb
<p>
<%= f.label "gehört_zu" %><br />
<%= f.collection_select :parent_id, Page.all, :id, :menutext, :include_blank => ' - ' %>
</p>
# ../edit.html.erb
<p>
<%= f.label "gehört_zu" %><br />
<%= f.collection_select :parent_id, Page.all.reject { |p| p == @page }, :id, :menutext, :include_blank => ' - ' %>
</p>
oder
# ../edit.html.erb
<p>
<%= f.label "gehört_zu" %><br />
<%= f.collection_select :parent_id, Page.all, :id, :menutext, :include_blank => ' - ', :disabled => @page.id %>
</p>
Warum Collection-Select? ..
disabled oder reject ..
Darstellung
[Bearbeiten]Jetzt wirds ein bisschen komplizierter. Anders ausgedrückt, es gibt wieder was zu lernen. Auf den Seiten sollen nämlich Events, Texte oder andere Elemente wie z.B. eine Bildergalerie dargestelt werden. Die Events sollen Monats- oder Jahresweise zusammengefasst dargestellt werden. Auch die Textblöcke wollen wir zu Gruppen zusammnenfassen und dann eine ganze Gruppe auf einer Seite und darstellen können. Für das Datenmodell dazu bieten sich zwei Möglichkeiten an:
Erste Variante: Individuelle ContentRenderer mit Content
[Bearbeiten]Zu jeder Klasse die wir auf einer Seite darstellen wollen schreiben wir einen Renderer. Also z.B.: TextBlockRenderer, EventsPerMonthRenderer, EventsPerYearRenderer, ImageGalerieRenderer ..
Alle diese Renderer sind von eine Klasse (Renderer) mit SingleTableInheritace abgeleitet. Und jede Seite kann mehrere Renderer enthalten. Also
- Page has_many Renderers (Rendrer sind contentspezifisch)
- XyRenderer has XyContentStore
Zweite Variante: Jeder Content wird mit RenderModul renderfähig
[Bearbeiten]Jede Klasse, die wir auf einer Seite darstellen wollen wird mit einem Modul "Renderable" erweitert. Damit können sich alle Content-Objekte selbst darstellen. Und jede Seite kann mehrere ContentElemente direkt enthalten. Also
- Page has_many ContentStores (individual an independent Classes for Texts, Events, Images ...)
- every ContentStores
Besondere Herausforderung: Die ContentStores sind so unterschiedlich, dass STI keinen Sinn macht. D.h. 'Rails unterstützt diese Form der polymorphen Beziehung nicht. Wir müssen das aber nicht selbst lösen. "has_many_polymorphs" ist ein Plugin, das das für uns erledigt.
Exkurs: Modelierung von Polymorphie unter Rails
[Bearbeiten]TODO: Theorie Kapitel
- http://railsforum.com/viewtopic.php?id=1422
- http://stackoverflow.com/questions/1054892/inheritance-and-polymorphism-conflicting-in-ruby-on-rails
- http://basic70tech.wordpress.com/2007/05/10/rails-belongs_to-polymorphic-and-inheritance/
- http://blog.evanweaver.com/articles/2006/07/28/has_many_polymorphs-gets-official/
- http://benjamin.francisoud.googlepages.com/googlecalendar
Besser?
- http://m.onkey.org/2007/8/14/excuse-me-wtf-is-polymorphs
- http://blog.evanweaver.com/files/doc/fauna/has_many_polymorphs/files/README.html
Ausserdem benötigen wir eine Kalenderdarstelung
Kalenderdarstellung
[Bearbeiten]Auch für die Kalenderdarstelung benutzen wir ein Pugin. Es gibt viele Alternativen
- http://github.com/clemens/later_dude/tree/master
- http://github.com/topfunky/calendar_helper/tree/master
- http://code.google.com/p/calendardateselect/
- http://github.com/dmix/weekly_builder/tree/master
- http://www.railslodge.com/tags/plugin/Calendar
- ..
Das Standardplugin ist
Eine Musteranwendung:
Wir eintscheiden uns aber nicht für ds Standartplugin, sondern für
- LaterDude (http://github.com/clemens/later_dude/tree/master)
Es ist vom Standardplugin abgeleitet, aktuell und - für uns wichtig - für Internationalisierung vorbereitet.
Buch über Plugins
Ok legen wir los ...
[Bearbeiten]Zunächst die Kalenderdarstellung. Wir installieren das Plugin und erzeugen die ContentSpeicher, die wir haben wollen. Zu jedem Contentspeicher erzeugen wir noch einen controller, damit wir die Darstelung testen können.
script/plugin install git://github.com/clemens/later_dude.git
# environment.rb
..
config.gem "has_many_polymorphs"
script/generate model EventsPerMonth script/generate controller EventsPerMonth show script/generate model EventsPerYear script/generate controller EventsPerYear show
SeitenModell
[Bearbeiten]kein Wildwuchs, wir benutzen has_many_polymorphs. Wenn wir has_many_polymorphs als gem installieren steht es uns in auch in anderen Projekten zur Verfügung.
sudo gem install has_many_polymorphs
Wir müssen das Seitenmodel erweitern,
class Page < ActiveRecord::Base
acts_as_tree
has_many_polymorphs :renderables, :from => [:textblocks, :events_per_months]
end
ein Join-Modell anlegen
script/generate model PagesRenderable
class PagesRenderable < ActiveRecord::Base
belongs_to :page
belongs_to :renderable, :polymorphic => true
end
und die Migration dazu:
class CreatePagesRenderables < ActiveRecord::Migration
def self.up
create_table :pages_renderables do |t|
t.references :renderable, :polymorphic => true
t.references :page
end
end
def self.down
drop_table :pages_renderables
end
end
In den Rederable-Klassen (TextBlock, EventsPerMonth) müssen wir nicht ändern. NICHTS!!!
Prüfen wir ob alles klappt:
test "page can have textblocks" do
tb1 = Textblock.create :content => "asdf gh"
tb2 = Textblock.create :content => "jklö qw"
testpage = Page.create :menutext => "testpage"
testpage.renderables << tb1 <<tb2
assert_equal 2, testpage.renderables.size
assert_equal "asdf gh", testpage.renderables.first.content
assert_equal "asdf gh", testpage.textblocks.first.content
end
assert_equal 2, testpage.renderables.size
assert_equal "asdf gh", testpage.renderables.first.content
assert_equal "asdf gh", testpage.textblocks.first.content
end
TODO / Anmerkungen:
- Polymorphiedeklaration ist schlecht. Deshalb EventsPerMonth, .. Name ändern? EventsPerMonthBlock wäre klarer
- Wenn neue renderbare Klassen dazukommen muss immer die Polymorphiedeklaration erweitert werde. Das ist ein Problem. Das bei der alternativen Modelierung nicht auftritt. Zumindest dann nicht, wenn uns das renderable Interface genügt.