Hlavní navigace

Ruby on Rails: Blog podruhé

30. 12. 2005
Doba čtení: 9 minut

Sdílet

Minule jsme si ukázali, co vše lze dělat s objekty ActiveRecord. Dnes modely navážeme na kontroléry a pohledy, aby byl blog editovatelný přes web a současně vyřešíme přihlašování. Konečně se také dočkáte screenshotů.

Cíl tohoto dílu je následující:

  1. 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.
    Sign up
  2. Uživatel se přihlásí pomocí loginu a hesla, zobrazí se menu.
    Login
    Menu
  3. Uživatel bude moci vytvořit svůj blog či blogy.
    Blogs
  4. Po kliknutí na konkrétní blog bude uživatel moci editovat jednotlivé články blogu.
    Blog articles
  5. Po skončení práce se uživatel odhlásí.
    Logout

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 ApplicationCon­troller (sdílený všemi kontroléry) takto:

app/controller­s/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/controller­s/user_contro­ller.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 ad­min.

Jak na to? Postup si ukážeme na kontroléru přihlašování. Vytvoříme podadresář app/controller­s/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::UsersCon­troller. 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/controller­s/admin/admin_con­troller.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/lay­outs/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/controller­s/admin/users_con­troller.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_de­fault, 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 adminusers.

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/controller­s/admin/blogs_con­troller.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/controller­s/admin/blog_ar­ticles_contro­ller.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_ar­ticles/_form.rhtml

CS24_early

<!--[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.

Byl pro vás článek přínosný?