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::GalleriesController (z kontroléru Admin::BlogsController) 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://localhost:3000/admin/galleries. 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.
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::PhotosController zkopírováním Admin::BlogArticlesController, 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]' />
...
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/photos. 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
- 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é. - filename a thumbnail_filename
Slouží k sestavení názvů souborů snímků a náhledů. - 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. - 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. - 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. - 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::PhotosController, který se opravdu liší od Admin::BlogArticlesController 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/photos. 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_helper.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):
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:
Příště se podíváme na zobrazování galerií na stránkách. Aktuální verzi webu je možno stáhnout.