Hlavní navigace

Ruby on Rails: Galerie poprvé

27. 1. 2006
Doba čtení: 10 minut

Sdílet

Dnes se budeme zabývat administrační částí galerie. Princip bude obdobný jako u blogů, ale přibudou dynamicky generované náhledy. Každý uživatel bude moci vytvořit libovolný počet galerií, přičemž rozkliknutím konkrétní galerie se mu zobrazí snímky.

Návrh je následující: Každý uživatel bude moci vytvořit libovolný počet galerií, přičemž rozkliknutím konkrétní galerie se mu zobrazí snímky té galerie. Podobně fungují blogy, kde je nejprve zobrazen seznam blogů a po rozkliknutí konkrétního blogu se ukážou články. Doufám, že se vám blogy podařilo minule rozchodit, poněvadž je budeme potřebovat i v tomto díle – jelikož je galerie jejich obdobou, prostě je zkopírujeme.

Nejprve vytvoříme dvě nové tabulky. První bude představovat seznam galerií, druhá bude seznamem fotografií. Porovnejte je s tabulkami blogs a blog_articles, víceméně se neliší.

create table galleries (
    id int auto_increment,
    user_id int,
    name varchar(255),
    description text,
    primary key(id),
    constraint fk_user foreign key (user_id) references users(id)
) 
create table photos (
    id int auto_increment,
    gallery_id int,
    ext varchar(5),
    fname varchar(255),
    title varchar(255),
    position int default 0,
    primary key(id),
    constraint fk_gallery foreign key (gallery_id) references galleries(id)
) 

Seznam galerií

Seznam galerií dostaneme zkopírováním seznamu blogů. Zkopírujeme kontrolér, pohledy a model. Vznikne tak kontrolér Admin::Galleri­esController (z kontroléru Admin::BlogsCon­troller) a model Gallery (z modelu Blog). Poté všude nahradíme výskyty slov blog za gallery, Blog za Gallery, blogs za galleries, atd. Řeklo by se, že je to snadné, přesto se mi osvědčilo vyhledávat jednotlivá slova „blog“ po jednom a neměnit je naráz, neboť vždy na něco zapomenu. Kontrolér a model by měl vypadat takto:

app/controllers/galleries_controller.rb
class Admin::GalleriesController < ApplicationController

  layout  'admin'
  before_filter :login_required

  def index
    list
    render :action => 'list'
  end

  def list
    @gallery_pages, @galleries = paginate :galleries, :per_page => 10
    @galleries = User.find( session[:user].id ).find_in_galleries( :all )
  end

  def new
    @gallery = Gallery.new
  end

  def create
    @gallery = session[:user].build_to_galleries( params[:gallery] )
    if @gallery.save
      flash[:notice] = 'Gallery was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end

  end

  def edit
    @gallery = session[:user].find_in_galleries( params[:id] )
  end

  def update
    @gallery = session[:user].find_in_galleries( params[:id] )
    if @gallery.update_attributes(params[:gallery])
      flash[:notice] = 'Gallery was successfully updated.'
      redirect_to :action => 'list'
    else

      render :action => 'edit'
    end
  end

  def destroy
    @gallery = session[:user].find_in_galleries( params[:id] ).destroy
    redirect_to :action => 'list'
  end

end 
app/models/gallery.rb
class Gallery < ActiveRecord::Base

        belongs_to :user

end 

Pozn.: Dříve jsem v akci create používal metodu create_in_*, ale nyní jsem ji nahradil za build_to_*. Rozdíl je v tom, že create_in volá zároveň i save, jenže save je použito také o řádek níž, čímž by se volalo zbytečně dvakrát. Build_to objekt pouze vytváří, avšak neukládá, pročež je výhodnější.

Se šablonami provedeme totéž, nebudu je zde uvádět (můžete si stáhnout tarball na konci). Většinou jde zase o nahrazování řetězců, snad jen ve _form.rhtml se musí nahradit „name“ za „title“ kvůli jinému pojmenování sloupců tabulky. Nakonec do menu v layoutu přidejte odkaz:

app/views/layouts/admin.rhtml
...
<li><%= link_to 'Galerie', :controller=>'galleries' %></li>

... 

V tuto chvíli by měl seznam galerií fungovat po přihlášení do administračního rozhraní na adrese http://localhos­t:3000/admin/ga­lleries. Nepředpokládám, že půjde napoprvé, nicméně z chybových hlášek snad poznáte proč. Zkuste vytvořit nějakou galerii.

galerie-list

Seznam fotografií

Seznam fotografií se bude zobrazovat po rozkliknutí názvu konkrétní galerie. Seznam připomíná články blogů, takže postupujeme obdobně jako u seznamu galerií; vytvoříme kontrolér Admin::Photos­Controller zkopírováním Admin::BlogAr­ticlesController, dále zkopírujeme pohledy a nahradíme výskyty slov blog za gallery, Blog za Gallery, BlogArticle za Photo, blog_articles za photos, atd. Nový model Photo získáme z modelu BlogArticle. Nakonec provážeme model Gallery s modelem Photo.

app/models/gallery.rb
class Gallery < ActiveRecord::Base

        belongs_to :user
        has_many :photos, :dependent=>true, :order=>'position DESC'

end 
app/models/photo.rb
class Photo < ActiveRecord::Base

        belongs_to :gallery

end 

Než přidáme zobrazování náhledů do seznamu a pole pro nahrání souboru do formuláře, zkusme seznam fotografií použít bez toho – pouze jako články blogů, tedy s editovatelným titulkem a popisem. Jestliže toto bude fungovat, můžeme pokračovat.

Nyní přidáme pole pro nahrání souboru. Jednak je nutné v šablonách new.rhtml a edit.rhtml upravit form tag, aby obsahoval enctype, za druhé je nezbytné doplnit do společné šablony _form.rhtml samotné pole pro nahrání souboru.

app/views/admin/photos/new.rhtml
...
<form action="<%= url_for :action => 'create' %>" method='post' enctype='multipart/form-data'>
... 
app/views/admin/photos/edit.rhtml
...
<form action="<%= url_for :action => 'update', :id => @photo %>" method='post' enctype='multipart/form-data'>
... 
app/views/admin/photos/_form.rhtml
...
<input type='file' name='photo[file]' />
... 
photo-new

Jak funguje upload?

Narozdil od PHP, kde dostáváme soubory v poli $_FILES, lépe řečeno názvy souborů, v kontroléru Rails nalezneme soubory ve vstupních parametrech nikoli jako názvy, nýbrž jako již instancované IO objekty. Potomek třídy IO obvykle reprezentuje otevřený soubor, socket, či jiný stream, v našem případě reprezentuje nahraný soubor. Data či jiné informace z něj získáme jednoduše:

io = params[:photo]['file']
data = io.read
size = io.size
name = io.original_filename 

Fotografie a náhledy

Snímky budeme ukládat fyzicky na disk, všechny se budou nalézat v adresáři public/images/pho­tos. Ne přímo v něm, ale ještě o úroveň níž – v podadresářích pojmenovaných podle id snímků. Jeden snímek bude reprezentován jedním podadresářem. Uvnitř podadresářů nalezneme jak originální snímek, tak jeho náhledy. Pro sestavení názvů souborů navíc využijeme titulků snímků, takže například uvidíme ne jen „23.jpg“, nýbrž „23-silvestr_2005.jpg“ či „23-silvestr_2005–100×200.jpg“ (přejmenovávání souborů v případě změny titulku snímku po editaci vyřešíme též).

Přidávání snímků bez pole pro vložení souboru nám zatím fungovalo. Od doby, co jsme ho přidali (jmenuje se „file“), však proces selže, protože v tabulce žádný sloupec file neexistuje. Jelikož voláme build_to_photos( params[:photo] ) a v poli params[:photo] je proměnná file obsažena, Rails očekávají, že bude v modelu Photo existovat metoda „file=“. Problém je, že neexistuje. Existovala by, kdyby existoval odpovídající sloupec v tabulce, jenže takový sloupec v tabulce není a ani nebude. Metodu „file=“ napíšeme vlastní, jejím úkolem nebude žádný zápis do databáze, ale práce se snímkem.

Kde se budou generovat náhledy? Jednou z možností by byl helper, ale přijde mi výhodnější přímo model Photo, protože chci se snímky i jejich náhledy pracovat na jednom místě. Uvedu celý model Photo a poté popíšu jednotlivé metody.

app/models/photo.rb
class Photo < ActiveRecord::Base

        belongs_to :gallery

        PHOTO_DIR = "#{RAILS_ROOT}/public/images/photos"

        def file= io
                # io = IO of uploaded file
                return false if ! io.respond_to?( :read ) || io.eof?
                @io = io
        end

        def filename
                "#{self.id}-#{self.fname}.#{self.ext}"
        end

        def thumbnail_filename width, height
                "#{self.id}-#{self.fname}-#{width}x#{height}.#{self.ext}"
        end

        def create_thumbnail width, height
                tn_filename = thumbnail_filename( width, height )
                original_path  = "#{PHOTO_DIR}/#{self.id}/#{filename}"
                thumbnail_path = "#{PHOTO_DIR}/#{self.id}/#{tn_filename}"
                if ! File.exists?( thumbnail_path )
                        require 'RMagick'
                        original = Magick::ImageList.new( original_path )
                        thumbnail = original.resize( width, height )
                        thumbnail.write( thumbnail_path )
                end

                tn_filename
        end

        def before_save
                if @io
                        # uploaded new file, preserve extension
                        @io.original_filename.match( /^.*\.([^.]+)$/ )
                        self.ext = $1.gsub( /[^\w]/, '_' )
                end
                @orig_filename = filename
                # TODO: odcesteni
                self.fname = self.title.gsub( /[^\w]/, '_' )
        end

        def after_save
                if @io
                        # new file uploaded, destroy image and all it's thumbnails
                        destroy_files
                        Dir.mkdir( "#{PHOTO_DIR}/#{self.id}" )
                        File.open( "#{PHOTO_DIR}/#{self.id}/#{filename}", "w+") { |f|
                                @io.rewind # safer
                                f.write( @io.read )
                        }
                else
                        # old file, but new title may differ => if so, we have to rename files
                        if @orig_filename != filename
                                # TODO: rename thumbnails
                                File.rename( "#{PHOTO_DIR}/#{self.id}/#{@orig_filename}", "#{PHOTO_DIR}/#{self.id}/#{filename}" )
                        end

                end
        end

        def destroy_files
                ` rm -Rf "#{PHOTO_DIR}/#{self.id}/" `
        end

        def after_destroy
                destroy_files
        end

end 
  1. file=
    Úkolem této metody je zapamatovat předaný IO objekt při build_to_photos k pozdějšímu použití. Taktéž otestuje, zda se vůbec jedná o IO objekt a zda je tento IO objekt otevřený (použitelný). Bohužel neplatí, že když odešleme prázdné vstupní pole souboru, bude io rovno nil či false. Namítnete, že bychom ho mohli testovat pomocí io.kind_of?( IO ). Občas je tímto objektem StringIO, u něhož však kind_of? vrací false. Napadlo mne proto řešení, že se prostě dotážu, zda má objekt metodu read. Jednoduché, ale účinné.
  2. filename a thumbnail_file­name
    Slouží k sestavení názvů souborů snímků a náhledů.
  3. create_thumbnail
    Vytvoří náhled s využitím ImageMagicku (ImageMagick musí být nainstalován, například jako gem). Tuto metodu je možné volat, až když je snímek uložen, jinak vyvolá chybu.
  4. before_save
    Ve chvíli before_save jsou již nastaveny všechny atributy modelu (pojmenované podle sloupců tabulky) podle params[:photo]). Pokud existuje @io, neboli byl nahrán soubor, zaznamená se koncovka souboru. Též se zaznamená původní název – když nebyl nahrán soubor, ale jen změněn titulek snímku, bude nutné soubor později přejmenovat, k čemuž je třeba znát i původní název.
  5. after_save
    Jestliže existuje @io, neboli byl nahrán soubor, uloží ho, přičemž napřed smaže celý adresář i se všemi náhledy. Nebyl-li nahrán soubor, zřejmě byl editován jen titulek snímku, v tom případě přejmenuje soubor snímku. Metoda je volána automaticky po save.
  6. after_destroy
    Po smazání záznamů z tabulek je nutné uklidit i na disku. Metoda je volána automaticky po destroy.

Veškerou práci s náhledy tedy obstarává model, uvedu zde ještě kontrolér Admin::Photos­Controller, který se opravdu liší od Admin::BlogAr­ticlesController jen v přejmenovaných proměnných, konstantách a staronové akci show.

app/controllers/admin/photos_controller.rb
class Admin::PhotosController < ApplicationController

        layout  'admin'

        def index
                list
                render :action => 'list'
        end

        def list
                if params[:id]
                        session[:gallery_id] = params[:id]
                end
                @photo_pages, @photos = paginate :photos, :per_page => 10
                @gallery = User.find( session[:user].id ).find_in_galleries( session[:gallery_id] )
                @photos = @gallery.find_in_photos( :all )
        end

        def new
                @photo = Photo.new
        end

        def create
                @photo = session[:user].find_in_galleries( session[:gallery_id] ).build_to_photos( params[:photo] )
                if @photo.save
                        flash[:notice] = 'Photo was successfully created.'
                        redirect_to :action => 'list'
                else

                        render :action => 'new'
                end
        end

        def edit
                @photo = session[:user].find_in_galleries( session[:gallery_id] ).find_in_photos(params[:id])
        end

        def show
                @photo = Photo.find( params[:id] )
        end

        def update
                @photo = session[:user].find_in_galleries( session[:gallery_id] ).find_in_photos(params[:id])
                if @photo.update_attributes(params[:photo])
                        flash[:notice] = 'Photo was successfully updated.'
                        redirect_to :action => 'list'
                else

                        render :action => 'edit'
                end
        end

        def destroy
                session[:user].find_in_galleries( session[:gallery_id] ).find_in_photos(params[:id]).destroy
                redirect_to :action => 'list'
        end

end 

V tuto chvíli by již mělo vše zase fungovat, nové snímky by měly být ukládány do adresáře public/images/pho­tos. Jak je ale zobrazit? Napíšeme dva jednoduché helpery. Účelem helperu „photo“ bude zobrazit snímek v originální velikosti, „thumbnail“ zobrazí jeho náhled po jehož rozkliknutí se objeví detail (zatím jen původní velikost). Oba helpery zapíšeme do application_hel­per.rb, aby byly dostupné ve všech šablonách.

app/helpers/application_helper.rb
...
       def photo ph
                src = "/images/photos/#{ph.id}/#{ph.filename}"
                return <<-EOF
                        <img src='#{src}' alt='#{ph.title}' />

                EOF
        end

        def thumbnail photo, width, height, href=nil
                begin
                        thumbnail_filename = photo.create_thumbnail( width, height )
                rescue
                        return <<-EOF
                                <p>ERR: Can't create thumbnail</p>

                        EOF
                else
                        href ||= url_for :controller=>'photos', :action=>'show', :id=>photo.id
                        src = "/images/photos/#{photo.id}/#{thumbnail_filename}"
                        return <<-EOF
                                <a href='#{href}' title='detail ilustrace'>
                                        <img src='#{src}' alt='#{photo.title}' />
                                </a>

                        EOF
                end
        end
... 

A použití v šablonách? Takto:

<%= photo @photo %>

<%= thumbnail photo, 100, 200 %>
<%= thumbnail photo, 200, 300 %> 

Například při editaci snímku se objeví nad formulářem jeho náhled (viz akce edit, kde se nastaví @photo):

CS24_early

photo-edit
app/views/admin/photos/_form.rhtml
<%= error_messages_for 'photo' %>

<!--[form:photo]-->

<% if @photo.id %>
    <%= thumbnail @photo, 100, 200 %>
<% end %>

<p><label for="photo_title">Title</label><br/>
<%= text_field 'photo', 'title'  %></p>

<input type='file' name='photo[file]' />

<br />

<!--[eoform:photo]--> 

Závěr

Po tom všem by mělo vylistování galerie vypadat nějak takto:

photos-list

Příště se podíváme na zobrazování galerií na stránkách. Aktuální verzi webu je možno stáhnout.

Seriál: Ruby on Rails

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