Cíl tohoto dílu je následující:
- Uživatelé se budou registrovat sami. Zatím jednoduše, bez jakýchkoli potvrzovacích e-mailů či obrázků (nakonec, kdo má potvrzovací e-maily rád, že?) Obrázek k opsání přidáme později.
- Uživatel se přihlásí pomocí loginu a hesla, zobrazí se menu.
- Uživatel bude moci vytvořit svůj blog či blogy.
- Po kliknutí na konkrétní blog bude uživatel moci editovat jednotlivé články blogu.
- Po skončení práce se uživatel odhlásí.
Přihlašování
Přihlašování je s Rails triviální, nainstalujte login generátor online pomocí gemu.
gem install login_generator
Poznamenávám, že existuje více login generátorů či pluginů řešících přihlašování. Přesto mi login generátor vyhovuje svou jednoduchostí, takže ho uvádím.
mig> script/generate login
Usage: script/generate login LoginName [options]
Jako LoginName jsem použil Users, poněvadž model loginu se vždy jmenuje User a chci, aby se i kontrolér jmenoval podle toho – UsersController. Navíc tabulku users už máme. Model User též existuje, doporučuji ho zazálohovat – bude za chvíli přepsán, ale poté se bude hodit.
mig> script/generate login Users
create lib/login_system.rb
overwrite app/controllers/users_controller.rb? [Ynaq] y
force app/controllers/users_controller.rb
...
Aby se stal přihlašovací systém aktivní, je vhodné upravit pseudokontrolér ApplicationController (sdílený všemi kontroléry) takto:
app/controllers/application.rb
# Filters added to this controller will be run for all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
require_dependency "login_system"
class ApplicationController < ActionController::Base
include LoginSystem
end
Podívejme se do modelu User. Generátor přihlášení ho kompletně přepsal, takže do něj musíme vrátit, co v něm bylo – definici reference:
app/controllers/user_controller.rb
...
has_many :blogs, :dependent=>true, :order=>'name DESC'
...
Nyní se zkuste zaregistrovat na adrese http://localhost:3000/users/signup
, čímž by mělo dojít současně k přihlášení. Po odhlášení se znovu přihlásíte na adrese http://localhost:3000/users/login
.
Poznámka: Nemůžete-li se přihlásit, viníkem je zřejmě omezená délka hesla v definici tabulky, kterou je třeba upravit na „password varchar(40)“. Hesla jsou totiž ukládána jako SHA1 hashe, tím pádem jsou dlouhá.
Administrační rozhraní
Protože administrační rozhraní představuje cosi jako stránky ve stránkách, rozhodl jsem se, že pro větší přehlednost přemístím všechny kontroléry a pohledy, které mají něco společného s administrací, do podadresáře admin. Například blogy budeme jak administrovat, tak je zobrazovat na stránkách. Osobně preferuji dva kontroléry používající stejný model před jediným kontrolérem obsluhujícím jak administratvní, tak zobrazovací část. Jeden kontrolér tedy bude tam, kde je a druhý v podadresáři admin.
Jak na to? Postup si ukážeme na kontroléru přihlašování. Vytvoříme podadresář app/controllers/admin a soubor kontroléru users_controller.rb do něj přesuneme. Pouze v něm upravíme název třídy z UsersController na Admin::UsersController. A je to. Adresa se trochu změní: http://localhost:3000/admin/users/login
Totéž provedeme s blogs a blog_articles, takže v adresáři app/controllers
by měly zůstat pouze dva soubory, application.rb
a guestbook_controller.rb
, zbytek se bude nacházet v adresáři app/controllers/admin
.
Pojďme vytvořit layout administračního rozhraní, jehož funkce bude podobná jako layoutu stránek – bude zobrazovat společné menu. Zároveň upravíme kontrolér přihlášení, aby se po zalogování zobrazilo právě toto menu. Jinak řečeno, vytvoříme kontrolér Admin, který bude znát pouze akci index, a ta se právě zobrazí po přihlášení (bude prázdná, takže uvidíme pouze layout – menu).
mig> script/generate controller Admin::Admin index
Kontrolér adminu musí vyžadovat přihlášení. Všechny jeho akce budou od nynějška chráněny heslem. Docílíme toho přidáním metody before_filter (metoda akceptuje i parametr „:except“, jímž vyloučíme některé akce).
app/controllers/admin/admin_controller.rb
class Admin::AdminController < ApplicationController
layout 'admin'
before_filter :login_required
def index
end
end
Dále layout, který bude fungovat jako menu. V adresáři layouts doporučuji smazat všechny layouty kromě dvou – layoutů stránek a administračního rozhraní – rolldance.rhtml a admin.rhtml.
app/views/layouts/admin.rb
<html>
<head>
<title>Admin: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<% if @session[:user] %>
<h1>menu</h1>
<ul>
<li><%= link_to 'Moje blogy', :controller=>'blogs' %></li>
<li><%= link_to 'Logout', :controller=>'users', :action=>'logout' %></li>
</ul>
<% end %>
<p style="color: green"><%= flash[:notice] %></p>
<%= @content_for_layout %>
</body>
</html>
Pak ještě zbývá pozměnit kontrolér přihlášení:
app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
layout 'admin'
def login
if session[:user]
redirect_back_or_default :controller=>'admin'
elsif @request.method == :post
if @session[:user] = User.authenticate(@params[:user_login], @params[:user_password])
flash['notice'] = "Login successful"
redirect_back_or_default :controller=>'admin'
else
flash.now['notice'] = "Login unsuccessful"
@login = @params[:user_login]
end
end
end
def signup
@user = User.new(@params[:user])
if @request.post? and @user.save
@session[:user] = User.authenticate(@user.login, @params[:user][:password])
flash['notice'] = "Signup successful"
redirect_back_or_default :controller=>'admin'
end
end
def logout
@session[:user] = nil
end
end
Jaké změny jsem v něm provedl? Jednak jsem upravil akci login, aby nezobrazila přihlašovací formulář, když už je uživatel přihlášen, zadruhé jsem změnil parametry metody redirect_back_or_default, aby přesměrovávala na kontrolér admin. Upozorňuji, že je nutné psát jména kontrolérů v podadresáři admin
„relativně“, tedy nikoli admin/admin
, admin/users
, nýbrž jen admin
, users
.
Také upravte soubor lib/login_system.rb a v něm metodu access_denied, jež uživatele přesměruje někam jinam, pokud není přihlášen a požadoval akci chráněnou přihlášením.
lib/login_system.rb
...
def access_denied
redirect_to :controller=>"/admin/users", :action =>"login"
end
...
Na adrese http://localhost:3000:/admin/admin
by se mělo objevit menu administračního rozhraní nebo přihlašovací formulář (nejste-li přihlášeni).
Blogy
V současném stavu blogy sdílí všichni uživatelé, což je špatně. Zkuste vytvořit blog, pak se přihlašte jako jiný uživatel a blog uvidíte též. Kontrolér blogů bude po změně vypadat takto:
app/controllers/admin/blogs_controller.rb
class Admin::BlogsController < ApplicationController
layout 'admin'
before_filter :login_required
def index
list
render :action => 'list'
end
def list
@blog_pages, @blogs = paginate :blogs, :per_page => 10
@blogs = User.find( session[:user].id ).find_in_blogs( :all )
end
def new
@blog = Blog.new
end
def create
#@blog = Blog.new( params[:blog] )
@blog = session[:user].create_in_blogs( params[:blog] )
if @blog.save
flash[:notice] = 'Blog was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
def edit
#@blog = Blog.find( params[:id] )
@blog = session[:user].find_in_blogs( params[:id] )
end
def update
#@blog = Blog.find(params[:id])
@blog = session[:user].find_in_blogs( params[:id] )
if @blog.update_attributes(params[:blog])
flash[:notice] = 'Blog was successfully updated.'
redirect_to :action => 'list'
else
render :action => 'edit'
end
end
def destroy
#@blog = Blog.find(params[:id]).destroy
@blog = session[:user].find_in_blogs( params[:id] ).destroy
redirect_to :action => 'list'
end
end
Pro zřetelnost jsem zakomentoval původní řádky. session[:user] obsahuje instanci modelu User, která byla do session vložena v kontroléru přihlášení (vrátila ji metoda User.authenticate v případě úspěšného přihlášení). To znamená, že není-li uživatel přihlášen, session[:user] je rovno nil. Nemusíme proto volat User.find( session[:user].id ), objekt již je instancován (otázkou je, zda obsahuje aktuální údaje, v našem případě však ano).
Po této úpravě by již měly být blogy jednotlivých uživatelů různé. Jednoduché, že? Obdobným způsobem upravíme kontrolér článků blogů.
app/controllers/admin/blog_articles_controller.rb
class Admin::BlogArticlesController < ApplicationController
layout 'admin'
def index
list
render :action => 'list'
end
def list
if params[:id]
session[:blog_id] = params[:id]
end
@blog_article_pages, @blog_articles = paginate :blog_articles, :per_page => 10
@blog = User.find( session[:user].id ).find_in_blogs( session[:blog_id] )
@blog_articles = @blog.find_in_blog_articles( :all )
end
def new
@blog_article = BlogArticle.new
end
def create
@blog_article = session[:user].find_in_blogs( session[:blog_id] ).create_in_blog_articles(params[:blog_article])
if @blog_article.save
flash[:notice] = 'BlogArticle was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
def edit
@blog_article = session[:user].find_in_blogs( session[:blog_id] ).find_in_blog_articles(params[:id])
end
def update
@blog_article = session[:user].find_in_blogs( session[:blog_id] ).find_in_blog_articles(params[:id])
if @blog_article.update_attributes(params[:blog_article])
flash[:notice] = 'BlogArticle was successfully updated.'
redirect_to :action => 'list'
else
render :action => 'edit'
end
end
def destroy
session[:user].find_in_blogs( session[:blog_id] ).find_in_blog_articles(params[:id]).destroy
redirect_to :action => 'list'
end
end
Jakmile je blog_articles zavoláno s id, toto id se zapamatuje v session, pak ho již nepředáváme v url. Všiměte si též, že místo přímého vyhledávání v blogu pomocí id konkrétního blogu
@blog = Blog.find( session[:blog_id] )
používám konstrukci začínající objektem přihlášeného uživatele
@blog = session[:user].find_in_blogs( session[:blog_id] )
Jaký je důvod? V prvním případě by bylo možné podstrčit id cizího uživatele, jinými slovy editovat cizí články. Naproti tomu metoda find_in_blogs vyhledává jen blogy konkrétního uživatele. Nemá-li uživatel blog s nějakým id, vyvolá chybu.
Dále jsem provedl pár kosmetických úprav. Jednak jsem zrušil akce show v blogs i blog_articles, zadruhé jsem přidal sloupec „created_on datetime“ do tabulky blog_articles a upravil šablonu, aby v ní šlo datum nastavovat.
app/views/admin/blog_articles/_form.rhtml
<!--[form:blog_article]-->
<p><label for="blog_article_title">Title</label><br/>
<%= text_field 'blog_article', 'title' %></p>
<p><label for="blog_article_body">Body</label><br/>
<%= text_area 'blog_article', 'body' %></p>
<p><label for="tests_created_on">Created on</label><br/>
<%= datetime_select 'tests', 'created_on' %></p>
<!--[eoform:blog_article]-->
Závěr
Příště dokončíme blog – vytvoříme výstup na stránkách mimo administrační rozhraní, předěláme knihu hostů a pohrajeme si s mapováním url na kontroléry a akce (tzv. pretty-url).
Kompletní web můžete stáhnout v jednom tarballu.