Uznaliśmy, że warto dalej drążyć temat Ruby on Rails. Aby przybliżyć faktyczne możliwości platformy, postanowiliśmy stworzyć przykładową aplikację. Będzie to jeden z tych programów, który niemal zawsze stanowi centralną część każdej strony WWW. System newsów to przykład aplikacji, na którą składa się wiele standardowych zagadnień z budowy stron internetowych. Zobaczmy czy w Rails można faktycznie szybko dojść do satysfakcjonujących rezultatów.
Aby rozpocząć pracę nad systemem newsów,
konieczne jest przyjęcie niezbędnych
założeń co do funkcjonalności aplikacji.
Zakładamy na początku, że przyjmie ona kształt
prostego systemu CMS. Niezbędne będzie zatem
rozdzielenie funkcjonalności aplikacji na dwie
części:
- Stronę WWW, której będą używali internauci,
pragnący skorzystać z oferowanych im treści. Do
dyspozycji użytkowników będzie kanał RSS generowany
automatycznie (w momencie żądania)
przez system. - Panel administracyjny, służący do zarządzania
zawartością publikowaną na stronie WWW. Ponieważ
konieczne jest zapewnienie limitowanego
dostępu do panelu administracyjnego, nasza
aplikacja musi zostać wyposażona w mechanizm
logowania oraz dostarczyć interfejs pozwalający
na rejestrację nowych użytkowników uprzywilejowanych
i na zarządzanie nimi.
Definiujemy składowe systemu poprzez określenie funkcjonalności panelu administracyjnego.
Panel narzuca sposób organizacji treści bez
określania warstwy prezentacji:
- Możliwość dodawania, edycji i usuwania aktualności.
- Możliwość dodawania, edycji i usuwania kategorii, do których przynależą newsy.
- Zastrzegamy, że aby wiadomość została
dodana, musi przynależeć do kategorii. Jest
to relacja wiele do jednego, czyli wiele newsów
należy do jednej kategorii. Nie będzie
możliwe przypisanie aktualności do więcej niż
jednej kategorii.
- Zastrzegamy, że aby wiadomość została
- Możliwość dodawania, edycji i zarządzania
kontami użytkowników.- Dodanie użytkownika polega na zalogowaniu
się na istniejące konto administratora i wypełnieniu
formularza. Składowymi formularza
są login i hasło służące do autentykacji oraz
takie pola jak imię, nazwisko, adres e-mail,
czy notatka \”o autorze\”. - System nie ma praw dostępu, stąd nowe
konto ma możliwości analogiczne do administratorskich.
- Dodanie użytkownika polega na zalogowaniu
Możliwe jest jednak rozszerzenie
systemu, tak by stworzyć konta typu redaktor
(tylko dodawanie) czy korektor (dodawanie i edycja).
Struktura panelu administracyjnego:
- Formularz logowania.
- Nowy news.
- Zarządzanie aktualnościami, lista aktualności z możliwością ich usunięcia i edycji.
- Formularz edycji aktualności.
- Nowy użytkownik, formularz.
- Zarządzanie użytkownikami, lista użytkowników z możliwością ich usunięcia.
- Mechanizm wylogowania.
Struktura strony WWW:
- Strona główna prezentująca ograniczoną odgórnie liczbę ostatnio dodanych aktualności bez podziału na kategorie, z zaznaczeniem przynależności do kategorii.
- Lista dostępnych kategorii z możliwością przejścia do listy aktualności w kategorii.
- Dostęp do kanału RSS przygotowanego wg standardu RSS 2.0.
- Strona kategorii, zawierająca listę skrótów aktualności przynależących do kategorii.
- Lista aktualności zostanie wygenerowana w oparciu o tytuł aktualności, wstęp (tzw. zajawkę). Po kliknięciu na aktualność otworzy się strona z jej pełną treścią.
- Lista zostanie ograniczona limitem aktualności na stronę. Będzie udostępniony mechanizm dzielenia na strony, dzięki któremu długość strony ulegnie skróceniu do rozsądnych rozmiarów oraz przyczyni się do uzyskania większej ilości odsłon.
- Strona aktualności, zawierająca pełną jej treść.
- News składa się z informacji o przynależności do kategorii z możliwością przejścia do listy newsów do niej przynależących
- News to tytuł, wstęp, treść oraz informacja o autorze.
- Pod newsem znajdzie się podpis, stanowiący część formularza do dodawania aktualności.
Docelowa aplikacja będzie zatem bardzo
prosta. Taki szablon systemu CMS to bardzo dobry
punkt wyjścia do dalszych eksperymentów. Proponowane
rozszerzenia systemu to:
- Obsługa zdjęć z generowaniem miniatur.
- Kodowanie hasła w bazie danych.
- Przydzielanie określonego dostępu dla użytkowników panelu, np. tylko do działu artykuły, akcji dodaj.
- Zastosowanie zaawansowanego wystroju graficznego strony.
Tworzymy projekt
Ponieważ większość naszych czytelników
korzysta z oprogramowania InstantRails, wszystkie
operacje będą bazowały na tym założeniu. Jeżeli
jednak ktoś wykorzystuje samodzielnie zainstalowaną
platformę Rails czy korzysta z niej na serwerze
zdalnym, także nie powinien mieć problemów
z wykonaniem tych samych operacji.
Uruchamiamy InstantRails i klikamy na \”Create
New Rails App…\”. Jako nazwę podajemy “news\”,
choć może to być dowolna nazwa:
rails news
cd news
Przechodzimy do katalogu z projektem, np.
{stala}C:/InstantRails/rails_apps/news{/stala} i dokonujemy
edycji pliku konfiguracji bazy danych: {stala}config/database.yml{/stala}.
development:
adapter: mysql
database: newsy_dev
username: root
password: root
host: localhost
Określamy parametry dostępu do bazy danych.
Dla tego projektu warto utworzyć osobną bazę
danych. Dobrą praktyką w Rails jest tworzenie
trzech środowisk pracy bazodanowej. Naturalnie,
na lokalnym komputerze stworzymy wyłącznie środowisko
developerskie, podczas gdy na serwerze
produkcyjnym moglibyśmy postawić bazy danych:
testową oraz produkcyjną.
Struktura naszej aplikacji będzie następująca:
• Ścieżka główna oraz katalog news/ to dostęp
publiczny do odczytywania newsów. Będzie
to zatem interfejs użytkownika. Przykładowe
adresy:
{stala}http://localhost:3000/{/stala} – spis ostatnich artykułów
{stala}http://localhost:3000/news/category/1{/stala} – artykuły w kategorii o identyfikatorze 1
{stala}http://localhost:3000/news/pokaz/1{/stala} – artykuł numer 1
• Panel administracyjny jest strefą chronioną, co
oznacza, że aby móc się do niej zalogować,
konieczne jest podanie loginu i hasła. Strefa
chroniona składa się z formularza logowania,
który pojawia się, ilekroć próbujemy uzyskać
dostęp do strony chronionych. Do panelu administracyjnego
wchodzimy, wprowadzając adres:
{stala}http://localhost:3000/admin/{/stala}
{stala}http://localhost:3000/admin{/stala} – zarządzanie użytkownikami
{stala}http://localhost:3000/article{/stala} – zarządzenia artykułami
{stala}http://localhost:3000/category{/stala} – zarządzanie kategoriami
Baza danych
Każdy projekt rozpoczynamy od utworzenia
potrzebnych modeli, które powstają w oparciu
o przemyślaną wcześniej strukturę bazy danych.
Potrzebujemy następujących tabeli w bazie:
- articles – artykuły
- categories – kategorie newsów
- users – użytkownicy
Ponieważ nasz system oddziela część administracyjną
od tej dla użytkownika, z tabeli articles będą korzystały
odpowiednio dwa modele – articles oraz news.
Utwórzmy zatem odpowiednie modele:
ruby scripts/generate model Article
ruby scripts/generate model Category
ruby scripts/generate model User
ruby scripts/generate model News
Wywołanie każdego z tych poleceń powoduje
utworzenie odpowiednich modeli. Przechodzimy
zatem do katalogu {stala}db/migrate{/stala}, by określić strukturę
naszej bazy danych.
Migracje to znakomity mechanizm oferowany
przez framework RoR. Pozwala on na sprecyzowanie
struktury bazy danych, lecz w taki sposób,
by możliwe było jej utworzenie na dowolnej
bazie, np. MySQL czy PostgreSQL. Migracje są
więc niezależne od bazy danych. Wraz z rozwojem
każdego oprogramowania struktura bazy danych
się zmienia. Dodanie nowej kolumny przestaje
być łatwe w momencie, gdy pracujemy na trzech
bazach danych. System migracji ułatwia to zadanie,
wprowadzając wersjonowanie struktury bazy
danych, z możliwością tworzenia zmian i przywracania
poprzednich wersji.
RoR utworzył w katalogu db/migrate 4 pliki:
001_create_articles.rb
002_create_categories.rb
003_create_users.rb
004_create_news.rb
Możemy usunąć plik 004, gdyż nie będzie nam
potrzebny. W końcu model news i articles korzystają
z tej samej tabeli w bazie danych.
Tradycyjny kod SQL tworzonej bazy danych
wyglądałby następująco (w MySQL):
CREATE TABLE \'articles\' (
\'id\' int(11) NOT NULL auto_increment,
\'title\' varchar(150) NOT NULL,
\'intro\' text NOT NULL,
\'content\' text NOT NULL,
\'user_id\' int(11) NOT NULL default \'1\',
\'category_id\' smallint(6) NOT NULL default \'1\',
\'date\' datetime NOT NULL,
PRIMARY KEY (\'id\')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE \'categories\' (
\'id\' int(6) unsigned NOT NULL auto_increment,
\'name\' varchar(50) default NULL,
PRIMARY KEY (\'id\')
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE \'users\' (
\'id\' int(11) NOT NULL auto_increment,
\'login\' varchar(50) NOT NULL,
\'password\' varchar(50) NOT NULL,
\'firstname\' varchar(100) NOT NULL,
\'surname\' varchar(100) NOT NULL,
\'email\' varchar(100) NOT NULL,
\'note\' text NOT NULL,
PRIMARY KEY (\'id\')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
W RoR korzystamy z migracji. Otwórzmy do
edycji plik {stala}001_create_articles.rb{/stala}, by utworzyć
strukturę tabeli:
class CreateArticles < ActiveRecord:: Migration
def self.up
create_table :articles do |t|
t.column :title, :string, :limit=> 150
t.column :intro, :text
t.column :content, :text
t.column :user_id, :integer
t.column :category_id, :integer
t.column :date, :datetime
end
end
def self.down
drop_table :articles
end
end
Wyróżniamy dwie metody, self.up – tworzenie
tabeli oraz self.down, powrót do sytuacji
sprzed jej powstania. W RoR domyślną kolumną
{stala}auto_increment{/stala} jest kolumna id, której tutaj
nie ma, gdyż RoR utworzy ją automatycznie. Tę
zasadę określaliśmy mianem \”konwencja ponad
konfigurację\”. Dodanie kolumn odbywa się
poprzez określenie ich nazwy ({stala}:title{/stala}) oraz typu
danych, które reprezentuje (tu {stala}:string{/stala}). Można
dodatkowo określić limit znaków, które mogą na
daną kolumnę przypadać (:limit).
Dostępne typy danych to: {stala}:string{/stala}, {stala}:text{/stala}, {stala}:integer{/stala},
{stala}:float{/stala}, {stala}:decimal{/stala}, {stala}:datetime{/stala}, {stala}:timestamp{/stala}, {stala}:time{/stala}, {stala}:date{/stala}, {stala}:binary{/stala}, {stala}:boolean{/stala}
Create_table to tak zwana transformacja. Jej
zadaniem jest utworzenie nowej tabeli. Istnieją
jednak i inne transformacje, np. {stala}add_column{/stala}, która
służy do dodawania nowej kolumny:
add_column :haslo, :string, :default => \"masło\", :limit => \"150\"
Definicja tabeli user będzie prezentowała się
następująco:
create_table :users do |t|
t.column :login, :string, :limit => 50
t.column :password, :string, :limit => 32
t.column :firstname, :string, :limit => 50
t.column :surname, :string, :limit => 50
t.column :email, :string, :limit => 50
t.column :note, :text
end
Tabela categories będzie tą najmniej skomplikowaną:
create_table :categories do |t|
t.column :name, :string, :limit => 50
end
Jeżeli poprawnie ustanowiliśmy połączenie
z bazą danych, możemy dokonać migracji. RoR automatycznie
utworzy nasze tabele w bazie danych.
Koniec martwienia się o poprawny kod SQL czy
korzystania z phpMyAdmin:
rake db:migrate
Relacje między tabelami
ActiveRecord to mechanizm RoR, który pozwala
na dynamiczne określanie zależności między tabelami
w bazie danych. Ponieważ liczba możliwych
kombinacji jest określona, mechanizm ten działa
bardzo dobrze. Dostępne typy zależności to:
- {stala}belongs_to{/stala} – każdy artykuł należy do kategorii
- {stala}has_one{/stala} – jedna fotografia ma jeden opis
- {stala}has_many{/stala} – jeden artykuł ma wiele zdjęć, jedna kategoria ma wiele artykułów
- {stala}has_and_belongs_to_many{/stala} – jeden artykuł może znajdować się w wielu kategoriach
Jak widać, modele tworzą pomiędzy sobą
relacje. Musimy je zastosować do naszego projektu.
Wykorzystamy dwie relacje z powyższej listy,
gdyż nasz projekt korzysta tylko z {stala}belongs_to{/stala} oraz
has_many. Otwórzmy plik {stala}articles.rb{/stala} z katalogu
{stala}app/models{/stala}:
class Articles < ActiveRecord::Base
belongs_to :category
belongs_to :user
end
Jeden artykuł należy zawsze do kategorii. Jeden
artykuł zawsze należy do jakiegoś użytkownika.
Z kolei do kategorii należy zawsze wiele artykułów
({stala}category.rb{/stala}):
class Category < ActiveRecord::Base
has_many :articles
end
Uwaga. Do newsów ({stala}news.rb{/stala}) należy wiele
kategorii i wielu użytkowników, choć model News
nie odpowiada w bazie danych tabeli news, lecz
articles:
class News < ActiveRecord::Base
belongs_to :category
belongs_to :user
end
Model użytkownika możemy pozostawić w nienaruszonym
stanie.
Tworzymy kontrolery
Zanim nasza aplikacja zacznie działać, musimy
utworzyć szereg kontrolerów akcji. By przyspieszyć
tempo tworzenia aplikacji, wykorzystamy
mechanizm scaffold, który wygeneruje wszystkie
potrzebne formularze. Dzięki temu zabiegowi panel
administracyjny stworzymy dosłownie w ciągu
kilku minut. Będzie on zawierał wszystkie opcje
potrzebne do zarządzania zawartością: dodawanie,
edycję, usuwanie użytkowników, kategorii oraz
artykułów.
By utworzyć kontroler po stronie klienckiej,
wywołujemy następujące polecenia generatora:
ruby scripts/generate controller news
ruby scripts/generate controller rss
Kontroler rss będzie odpowiadał za generowanie
kanału RSS. Postanowiliśmy go oddzielić
od kontrolera news, by zachować przejrzystość,
w przypadku, gdy zdecydujesz się na rozszerzenie
aplikacji. Po stronie administracyjnej tworzymy
szereg kontrolerów:
ruby scripts/generate controller article
ruby scripts/generate controller user
ruby scripts/generate controller category
System logowania
By korzystać z panelu administracyjnego, musimy
utworzyć pierwszego użytkownika. To dlatego
warto najpierw stworzyć interfejs administracyjny
pozbawiony logowania. Poddajmy edycji plik {stala}user_controller.rb{/stala}:
class UserController < ApplicationController
scaffold :user
end
Możemy teraz uruchomić nasz program: {stala}http://localhost:3000/user/{/stala}. Lecz przecież założyliśmy, że
do panelu administracyjnego będziemy wchodzić
poprzez adres {stala}http://localhost:3000/admin/{/stala}.
Dlatego modyfikujemy ścieżki {stala}config/routes.rb{/stala}.
Dodajmy linie:
map.connect \'admin\', :controller => \"user\"
map.connect \'\', :controller => \"news\"
Dzięki temu zabiegowi wprowadzenie adresu
admin powoduje uruchomienie kontrolera user.
Przy okazji sprawimy, by przy wpisaniu http://localhost:
3000/ pojawił się kontroler news, dzięki czemu
możliwe będzie wyświetlenie listy aktualnych
wiadomości.
Załóżmy nowe konto. Teraz możemy przystąpić
do programowania. Utworzymy system autoryzacyjny,
pozwalający na logowanie się na stronę.
Edytujemy kontroler {stala}application.rb{/stala}, stanowiący
klasę, po której dziedziczymy we wszystkich kontrolerach.
Dodajemy deklarację metody {stala}check_authentication{/stala}
odpowiedzialnej za sprawdzenie
autoryzacji użytkownika:
def check_authentication
unless @session[:user]
session[:intended_action] = action_name
session[:intended_controller] = controller_name
redirect_to :controller => \"user\", :
action => \"signin\"
end
end
W momencie, gdy sesja użytkownika nie
istnieje, zostaje on przekierowany do kontrolera
user i akcji signin odpowiedzialnej za logowanie.
Dwie zmienne {stala}intended_action{/stala} i {stala}intended_controller{/stala}
pozwalają na zapamiętanie miejsca, z którego
logował się użytkownik. W ten sposób możliwe
będzie jego przekierowanie po udanym zalogowaniu
we właściwe miejsce.
Stwórzmy teraz odpowiednią klasę {stala}UserController{/stala},
nadpisując tę, którą przed chwilą tworzyliśmy:
class UserController < ApplicationController
before_filter :check_authentication, :except => [:signin]
scaffold :user
layout \'admin\'
def signin
if request.post?
user = User.find(:first, :conditions=> [\'login = ? AND password = ?\', params[:login], params[:password]])
if user.blank?
raise \"Błąd nazwa użytkownika lub hasło!\"
end
session[:user] = user.id
redirect_to :action => session[:intended_action],
:controller => session[:intended_controller]
end
end
def logout
session[:user] = nil
redirect_to :controller => \"user\"
end
end
Właściwość klasy {stala}before_filter{/stala} powoduje,
że każdorazowe odwołanie się do dowolnej
z metod danej klasy powoduje sprawdzenie, czy
użytkownik jest zalogowany. Jeżeli nie, zostaje on
odesłany do metody {stala}check_authentication{/stala} klasy
bazowej. Wyjątkiem jest sytuacja, gdy wybrano
akcję signin, odpowiedzialną za logowanie
użytkownika.
Zdefiniowana metoda signin sprawdza, czy
użytkownik przesłał dane metodą POST. Tworzymy
nowy obiekt klasy User, wywołując metodę
find (cecha frameworka), znajdującą i zwracającą
pierwszy rekord spełniający warunek loginu i hasła
podanego przez użytkownika. Tu params jest tablicą
superglobalną, zawierającą dane wprowadzane
metodą POST i GET przez klienta przeglądarki.
W przypadku nie znalezienia użytkownika, zwracany
jest błąd. Jeżeli użytkownik został odnaleziony,
następuje zapisanie do tablicy sesyjnej jego
identyfikatora oraz przekierowanie do żądanej
akcji. Wylogowanie, za które odpowiada metoda
logout, jest realizowane poprzez wyzerowanie
tablicy sesyjnej i przekierowanie użytkownika do
kontrolera user.
Przystąpmy do utworzenia formularza logowania.
Utwórzmy plik {stala}signin.rhtml{/stala} w katalogu
{stala}app/views/user{/stala}. Zawartość będzie następująca:
<%= form_tag(:action => \"signin\") %>
Login:
Hasło:
<%= end_form_tag %>
Do poprawnego zrozumienia powyższej
zawartości konieczna jest znajomość systemu
szablonowego RoR. Większość tagów HTML
w RoR generujemy za pośrednictwem specjalnie
do tego stworzonych funkcji. Funkcja form_tag
odpowiada tu za wygenerowanie tagu {html}