Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog. | 3 komentarze
Wyobraźmy sobie, że chcemy utworzyć bazę ogłoszeń, do których chcemy w łatwy i przyjemny sposób dodawać zdjęcia. W tej notce pokażę jak w dosyć prosty sposób wykorzystać Railsy do stworzenia prostego panelu do zarządzania takimi ogłoszeniami. Stworzymy formularz dodawania ogłoszeń, w którym oprócz możliwości dodawania ogłoszeń umieścimy prosty formularz, który umożliwi załadowanie zdjęcia na serwer i podpięcie go pod dodawane ogłoszenie. Oczywiście udostępnimy możliwość wyboru zdjęć już obecnych na serwerze, dla uproszczenia zakładamy, że zdjęcie może należeć tylko do jednego ogłoszenia.
Wszystko, czego potrzebujemy to:
Zacznijmy od stworzenia nowego projektu. Nie będę skupiał się na podstawach, na potrzeby tego opisu wykorzystam domyślną konfigurację railsów z bazą SQLite. W linii poleceń tworzymy nowy projekt:
$ rails demo $ cd demo
Od razu stworzymy bazę danych, aby potem nie trzeba było do tego wracać:
$ rake db:create
Mając przygotowany projekt, zaopatrzmy się w odpowiednie pluginy. W tym celu odpalamy następujące polecenia:
$ script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/ $ script/plugin install http://responds-to-parent.googlecode.com/svn/trunk
Środowisko mamy już przygotowane. Pora na utworzenie modeli. Ograniczymy się do najbardziej podstawowych rzeczy. Utworzymy model Advertisement, który będzie miał tytuł oraz opis. Potrzebny będzie również model Photo, który będzie miał atrybuty wymagane przez plugin attachment_fu. Wklepujemy zatem w konsoli:
$ script/generate model Advertisement name:string description:text
W przypadku modelu Photo nie będziemy podawać żadnych parametrów dla generatora:
$ script/generate model Photo
Po wygenerowaniu modeli, zanim zmigrujemy bazę danych, wyedytujemy plik migracji db/migrate/002_create_photos.rb, wypełniając go jak poniżej:
class CreatePhotos < ActiveRecord::Migration
def self.up
create_table :photos do |t|
t.column :parent_id, :integer
t.column :content_type, :string
t.column :filename, :string
t.column :thumbnail, :string
t.column :size, :integer
t.column :width, :integer
t.column :height, :integer
t.column :advertisement_id, :integer
end
end
def self.down
drop_table :photos
end
end
W tej chwili możemy już zmigrować bazę danych
$ rake db:migrate
Pora na skonfigurowanie modeli, utworzenie relacji jeden-wiele między ogłoszeniami i zdjęciami oraz przygotowanie modelu Photo do pracy z pluginem attachment_fu (wykorzystanie relacji wiele-wiele pozostawiam jako ćwiczenie). W tym celu edytujemy pliki modeli jak poniżej
# app/models/advertisement.rb class Advertisement < ActiveRecord::Base has_many :photos end
# app/models/photo.rb
class Photo < ActiveRecord::Base
belongs_to :advertisement
has_attachment :content_type => :image, # zalacznikiem jest obrazek
:storage => :file_system, # trzymamy go w systemie plikow
:max_size => 2.megabytes, # ograniczamy rozmiar do 2 MB
:resize_to => '1200x800>', # zmniejszamy plik wejsciowy
:thumbnails => {:thumb => '200x200>'} # tworzymy jedna miniature
validates_as_attachment # walidujemy plik po zaladowaniu na serwer, przed zapisem do bazy danych
# z powyzsza walidacja mialem problemy pod Windowsem, posiadaczom tego systemu radze poki co zakomentowac
# powyzsza linijke
end
W tej chwili mamy już skonfigurowaną relację oraz przygotowany model pod umieszczanie na serwerze plików. Pora na następną część
Do prawidłowej pracy będziemy potrzebować dwa kontrolery. Jeden z nich, nazwijmy go advertisements będzie odpowiedzialny za zarządzanie ogłoszeniami, drugi — photos — będzie zajmował się zdjęciami. Wykorzystamy mechanizm REST. W tym celu tworzymy kontrolery z linii poleceń:
$ script/generate controller Advertisements index show new edit create update destroy $ script/generate controller Photos index new edit create destroy
Aby skorzystać z REST musimy jeszcze dodać odpowiednie wpisy w pliku config/routes.rb:
ActionController::Routing::Routes.draw do |map| map.resources :advertisements map.resources :photos # ponizsze wpisy pozostawiamy map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end
Nadeszła pora na zaimplementowanie akcji potrzebnych do zarządzania naszymi zasobami. Są to standardowe akcje, pokażę zatem jedynie zawartość kontrolerów:
# app/controllers/advertisements_controller.rb
class AdvertisementsController < ApplicationController
def index
@advertisements = Advertisement.find(:all)
respond_to do |format|
format.html {}
format.xml { render :xml => @advertisements.to_xml }
end
end
def show
@advertisement = Advertisement.find(params[:id])
respond_to do |format|
format.html {}
format.xml { render :xml => @advertisement.to_xml }
end
end
def new
@advertisement = Advertisement.new
end
def edit
@advertisement = Advertisement.find(params[:id])
end
def create
@advertisement = Advertisement.new(params[:advertisement])
respond_to do |format|
if @advertisement.save
format.html { redirect_to advertisement_url(@advertisement) }
format.xml { render :xml => @advertisement.to_xml }
else
format.html { render :action => 'new'}
format.xml { render :xml => @advertisement.errors.to_xml }
end
end
end
def update
@advertisement = Advertisement.find(params[:id])
respond_to do |format|
if @advertisement.update_attributes(params[:advertisement])
format.html { redirect_to advertisement_url(@advertisement) }
format.xml { render :xml => @advertisement.to_xml }
else
format.html { redirect_to edit_advertisement_url(@advertisement) }
format.xml { render :xml => @advertisement.errors.to_xml }
end
end
end
def destroy
@advertisement = Advertisement.find(params[:id])
respond_to do |format|
if @advertisement.destroy
format.html { redirect_to advertisements_url }
format.xml { head :ok }
else
format.html { redirect_to advertisement_url(@advertisement) }
format.xml { redirect_to @advertisement.errors.to_xml }
end
end
end
end
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
def index
@photos = Photo.find(:all, :conditions => 'parent_id IS NULL') # wybieramy tylko zdjecia w oryginalnych rozmiarach
respond_to do |format|
format.html {}
format.xml { render :xml => @photos.to_xml }
end
end
def new
@photo = Photo.new
@advertisements = Advertisement.find(:all)
end
def create
@photo = Photo.new(params[:photo])
respond_to do |format|
if @photo.save
format.html { redirect_to photos_url }
format.xml { render :xml => @photo.to_xml }
else
format.html { render :action => 'new' }
format.xml { render :xml => @photo.errors.to_xml }
end
end
end
def destroy
@photo = Photo.find(params[:id])
respond_to do |format|
if @photo.destroy
flash[:notice] = 'Zdjęcie zostało usunięte'
format.html { redirect_to photos_url }
format.xml { head :ok }
else
flash[:error] = 'Wystąpił błąd podczas usuwania zdjęcia'
format.html { redirect_to photos_url }
format.xml { render :xml => @photo.errors.to_xml }
end
end
end
end
Warstwa modeli i kontrolerów jest już prawie gotowa. Pozostało dodanie widoków, za pomocą których będziemy mogli odczuć, czy to, co stworzyliśmy w ogóle działa.
W warstwie widoków ograniczę się do najprostrzego wyświetlania elementów, bez żadnych upiększaczy (przynajmniej na razie). Poniżej wklejam najprostsze możliwe widoki, które nie oferują nic poza podstawowymi opcjami. Widoki dla kontrolera advertisements_controller:
# app/views/advertisements/index.html.erb
<h1>Ogłoszenia</h1>
<ul>
<% for advertisement in @advertisements %>
<li>
<p><%= link_to h(advertisement.name), advertisement_url(advertisement) %></p>
<p><%= truncate(h(advertisement.description), 20) %></p>
<p><%= link_to 'Edytuj', edit_advertisement_url(advertisement) %></p>
<p><%= link_to 'Usuń', advertisement_url(advertisement), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
</li>
<% end %>
</ul>
<p><%= link_to 'Dodaj nowe', new_advertisement_url %></p>
# app/views/advertisements/new.html.erb
<h1>Nowe ogłoszenie</h1>
<%= error_messages_for :advertisement %>
<% form_for @advertisement do |f| %>
<p><label for="advertisement_name">Nazwa:</label></p>
<p><%= f.text_field :name %></p>
<p><label for="advertisement_description">Opis:</label></p>
<p><%= f.text_area :description, :cols => 60, :rows => 20 %></p>
<p><%= submit_tag 'Zapisz', :disable_with => "Proszę czekać..." %></p>
<% end %>
<p><%= link_to 'Powrót', advertisements_url %></p>
# app/views/advertisements/show.html.erb
<h1><%=h @advertisement.name %></h1>
<p><%=h @advertisement.description %></p>
<p><%= link_to 'Edytuj', edit_advertisement_url(@advertisement) %></p>
<p><%= link_to 'Usuń', advertisement_url(@advertisement), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
<p><%= link_to 'Powrót', advertisements_url %></p>
<hr/>
<% unless @advertisement.photos.empty? %>
<p>Zdjęcia:</p>
<ul>
<% for photo in @advertisement.photos %>
<li><%= link_to image_tag(photo.public_filename(:thumb)), photo.public_filename %></li>
<% end %>
</ul>
<% end %>
# app/views/advertisements/edit.html.erb
<h1>Edycja ogłoszenia</h1>
<%= error_messages_for :advertisement %>
<% form_for @advertisement do |f| %>
<p><label for="advertisement_name">Nazwa:</label></p>
<p><%= f.text_field :name %></p>
<p><label for="advertisement_description">Opis:</label></p>
<p><%= f.text_area :description, :cols => 60, :rows => 20 %></p>
<p><%= submit_tag 'Zapisz', :disable_with => "Proszę czekać..." %></p>
<% end %>
<p><%= link_to 'Powrót', advertisements_url %></p>
Widoki dla kontrolera photos_controller:
# app/views/photos/index.html.erb
<h1>Zdjęcia</h1>
<ul>
<% for photo in @photos %>
<li>
<p><%= link_to image_tag(photo.public_filename(:thumb)), photo.public_filename %></p>
<p><%= link_to 'Usuń', photo_url(photo), :method => :delete, :confirm => 'Czy jesteś pewien?' %></p>
</li>
<% end %>
</ul>
<p><%= link_to 'Dodaj nowe', new_photo_url %></p>
# app/views/photos/new.html.erb
<h1>Nowe zdjęcie</h1>
<%= error_messages_for :photo %>
<% form_for @photo, :html => {:multipart => true} do |f| %>
<p><label for="photo_uploaded_data">Plik ze zdjęciem:</label></p>
<p><%= f.file_field :uploaded_data %></p>
<p><label for="photo_advertisement_id">Ogłoszenie:</label></p>
<p><%= f.collection_select :advertisement_id, @advertisements, :id, :name, {:prompt => true} %></p>
<p><%= f.submit "Załaduj" %></p>
<% end %>
<p><%= link_to 'Powrót', photos_url %></p>
W tej chwili możemy już zobaczyć, jak to wszystko działa. Wystarczy odpalić serwer:
$ script/server
Po odpaleniu serwera wpisując w adresie przeglądarki adres http://localhost:3000/advertisements możemy administrować ogłoszeniami, natomiast pod adresem http://localhost:3000/photos możemy zarządzać zdjęciami.
No dobrze, ale na początku pisałem o dynamicznym ładowaniu zdjęć podczas pisania ogłoszenia oraz o możliwości wybierania zdjęć. Na pierwszy rzut pójdzie możliwość wyboru zdjęć spośród załadowanych i nie dołączonych do innych ogłoszeń
Aby umożliwić dołączanie zdjęć podczas dodawania czy edycji ogłoszenia, skorzystamy z możliwości, jakie dostarcza metoda has_many w modelu ogłoszeń, a dokładnie z settera photo_ids. W tym celu należy dodać kilka rzeczy do kontrolera ogłoszeń i zmodyfikować odpowiednio widoki.
W metodach new oraz edit dodajemy następującą linijkę (oczywiście przeczymy tutaj zasadzie DRY, jednak chodzi tutaj o jak najprostsze pokazanie, o co chodzi):
@photos = Photo.find(:all, :conditions => 'parent_id IS NULL')
W tej chwili możemy już pokazać w formularzu dodawania/edycji ogłoszeń dostępne zdjęcia wraz z check boxami do ich zaznaczania. W tym celu dodajemy do formularza w plikach app/views/advertisements/{new,edit} gdzieś w bloku form_for, najlepiej po text area:
<div id="photos"> <% for photo in @photos %> <p><%= image_tag photo.public_filename(:thumb) %></p> <p><%= check_box_tag 'advertisement[photo_ids][]', photo.id, @advertisement.photos.include?(photo) %> <% end %> </div>
Od tej pory będziemy mogli zaznaczać zdjęcia, które będziemy chcieli dodać do ogłoszenia (pamiętajmy o tym, że mamy relację jeden-wiele, więc jeżeli tworząc lub edytując jakieś ogłoszenie zaznaczymy zdjęcie wcześniej zajęte, zostanie ono przypisane tylko do nowo tworzonego/edytowanego ogłoszenia — aby to zmienić należy zamienić relację z jeden-wiele na wiele-wiele). Zostaje tylko jeden problem. Kiedy będziemy chcieli usunąć z jakiegoś ogłoszenia wszystkie zdjęcia i odznaczymy wszystkie dostępne w formularzu, po kliknięciu 'Dodaj' lub 'Edytuj' nic się nie stanie. Dzieje się tak dlatego, że w tym przypadku Railsy nie przekażą do kontrolera zawartości parametru advertisement[photo_ids][]. Jednak jest bardzo prosty sposób, by to zmienić. Do kontrolera advertisements_controller w akcji update dodajemy na początku następującą linijkę:
params[:advertisement][:photo_ids] ||= []
Spowoduje ona, że w przypadku, gdy odznaczymy wszystkie zdjęcia podczas edycji ogłoszenia, zostaną rzeczywiście usunięte z relacji.
Do zrobienia została nam jeszcze jedna część — dynamiczne ładowanie zdjęcia podczas dodawania lub edycji ogłoszenia i uaktualnianie formularza nowym zdjęciem. Nie da się tego zrobić przy pomocy samego AJAX-a, ponieważ ze względów bezpieczeństwa AJAX nie pozwala na przesyłanie lokalnych plików. Istnieje jednak prosty sposób, aby tę niedogodność ominąć. W tym celu utworzymy na stronie niewodoczny iframe i ustawimy na niego atrybut target formularza ładującego zdjęcie. Następnie, wykorzystując plugin responds_to_parent, uaktualnimy listę zdjęć w formularzu dotyczącym ogłoszenia.
Na początku zdefiniujmy sobie prosty layout oraz dołączmy potrzebne skrypty JavaScript. W tym celu tworzymy plik app/views/layouts/application.erb o nastepującej zawartości:
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Demonstracja dynamicznego ładowania plików, © 2008 GhandaL</title>
<%= javascript_include_tag :defaults %>
</head>
<body>
<% if flash[:notice] %>
<div style="border: 1px green"><%= flash[:notice] %></div>
<% end %>
<% if flash[:error] %>
<div style="border: 1px red"><%= flash[:error] %></div>
<% end %>
<%= yield :layout %>
</body>
</html>
W celu ładowania pliku posłużymy się fragmentami kodu widoków, tzw. partials. Zanim jednak dopracujemy widoki, zmodyfikujemy odpowiednio kontroler photos_controller, aby uaktualniał stronę, która wywołała akcję create. W tym celu modyfikujemy akcję create w pliku app/controllers/photos_controller.rb tak, by wyglądała jak poniżej:
def create
@photo = Photo.new(params[:photo])
respond_to do |format|
if @photo.save
format.html { redirect_to photos_url }
format.xml { render :xml => @photo.to_xml }
format.js do
responds_to_parent do
render :update do |page|
page.insert_html :bottom, "photos", :partial => 'photo', :object => @photo
end
end
end
else
format.html { render :action => 'new' }
format.xml { render :xml => @photo.errors.to_xml }
format.js do
responds_to_parent do
render :update do |page|
# jakis kod uaktualniajacy, np. bledem, strone
end
end
end
end
end
end
Teraz możemy się zająć stworzeniem odpowiednich partials. Po pierwsze stworzymy kod, który wyświetli nowo załadowane zdjęcie. Tworzymy plik app/views/photos/_photo.erb o zawartości:
<p><%= image_tag photo.public_filename(:thumb) %></p> <p><%= check_box_tag 'advertisement[photo_ids][]', photo.id, true %></p>
Do zrobienia została nam jeszcze jedynie modyfikacja widoków akcji new i edit kontrolera advertisements_controller. Gdzieś na końcu plików app/views/advertisements/{new,edit} dopisujemy następujący kod:
<iframe name="dummy" id="dummy" style="width:1px;height:1px;border:0"></iframe>
<% form_for :photo, :url => {:controller => :photos, :action => :create, :format => 'js'}, :html => {:target => 'dummy', :multipart => true} do |f| %>
<p><label for="photo_uploaded_data">Plik ze zdjęciem:</label></p>
<p><%= f.file_field :uploaded_data %></p>
<p><%= f.submit 'Dodaj' %></p>
<% end %>
I to już wszystko. Od tej pory dodając i edytując ogłoszenia można już ładować zdjęcia bez potrzeby przeładowywania strony.
Oczywiście powyższe rozwiązanie ma pewne wady. Przede wszystkim relacja jeden-wiele jest bardzo prymitywna w tym przypadku i zabrania dołączania jednego zdjęcia do kilku ogłoszeń. Jednak modyfikacja tego nie jest trudna, nie o tym jest również ta notka. Kod demonstracyjnej aplikacji możesz ściągnąć mojej strony. Mam nadzieję, że powyższy opis komuś się przyda. Jeśli zauważyłeś jakiś błąd lub chciałbyś wyrazić opinię o moim rozwiązaniu, zapraszam do komentowania.
Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Ruby on Rails, Techblog, Web Design. | 10 komentarzy
Ponieważ zaczęły mnie denerwować timeouty w przekazywaniu żądań z Apache do mongreli na Rootnode zacząłem rozglądać się za czymś lepszym. Pod uwagę brałem VPS-y w cenie do ok. 60zł za miesiąc. Postanowiłem w najbliższych miesiącach przetestować kilka z nich. Na pierwszy ogień poszedł ProVPS. Za tym rozwiązaniem przemawia lokalizacja serwerów w Polsce, w Gdańsku, a co za tym idzie niskie pingi, w granicach 35-45ms. Jednak na tym plusy się kończą (porównując do reszty ofert branych pod uwagę). Za 49 zł netto miesięcznie dostajemy gwarantowane 128 MB RAM-u, rozszerzane do 512 MB, 5 GB powierzchni dyskowej, 25 GB transferu miesięcznego oraz 1 adres IP. Na tle konkurencji trochę blado.
Wczoraj aktywowano mi VPS i spędziłem kilka ładnych godzin na konfigurowaniu poszczególnych części systemu. Postanowiłem uruchomić minimum usług, głównie po to, by zmieścić się w drakońskich limitach pamięci. Niestety musiałem zrezygnować z mySQL, ponieważ nie znalazłem sposobu na ograniczenie konsumpcji pamięci do akceptowalnego na tym serwerze poziomu — po uruchomieniu mysqld konsumował ok. 100 MB pamięci operacyjnej pomimo przeróżnych konfiguracji znalezionych w sieci i w dokumentacji mySQL. Zaminiłem mySQL na PostgreSQL i tutaj małe zaskoczenie — domyślna instalacja postgresa zużywa ponad połowę mniej pamięci niż mySQL!
Gdy baza danych była już gotowa, nadszedł czas na serwer WWW. Wybór oczywiście padł na nginx, o wiele lżejszy od Apache'a serwer plus load balancer. Na VPS-ie będą uruchamiane jedynie statyczne pliki oraz aplikacje w Railsach i Merbie (w liczbie max. 3 — patrz pamięć). Z konfiguracją nginxa nie było żadnych problemów, świetną konfigurację dla aplikacji railsowych w obsługą cache przygotował Ezra Zygmuntowicz. Konfiguracja ta dostępna jest na jego stronie.
Następną rzeczą do zrobienia była kompilacja Ruby, Rubygems, ImageMagick oraz instalacja Subversion. Oczywiście po drodze były mniejsze komplikacje — a to zabrakło biblioteki zlib, a to nie zainstalowałem libsqlite3-dev. W każdym razie po mniej więcej 5 godzinach miałem już działającego Postgresa, nginxa, SVN, Ruby+Rails i całą resztę potrzebnych do życia aplikacji.
Nadszedł czas na sprawdzenie, czy praca nie poszła na marne. Zgrałem pisaną ostatnio aplikację, zmigrowałem bazę, ustawiłem dwie instancje serwera thin i uruchomiłem je. Et voilà! Wszystko ruszyło i działa jak należy i, co najważniejsze, naprawdę dużo szybciej niż na Rootnode, gdzie aplikacja stała poprzednio.
Nieprzespana noc z konfiguracją serwera za mną, ale za miesiąc prawdopodobnie czeka mnie następna, ponieważ, jak pisałem na wstępie, mam w planie przetestowanie kilku dostawców, którzy mają mniejszy miesięczny abonament i gwarantują 2x więcej pamięci, co dla mnie jest rzeczą ważną. Zastanawiam się, czy sensowne będzie kupowanie VPS-a w USA. Na mojej liście jest Slicehost oraz SilverRack z całkiem niezłą ofertą za niewielkie pieniądze. Z drugiej strony za zachodnią granicą mamy Host Europe, również z ciekawą ofertą i niskimi pingami.
Pomimo dużo większych nakładów czasowych myślę, że nie wrócę już do dzielonego hostingu. VPS i serwer dedykowany daje tę wolność, jakiej nie ma na żadnym dzielonym hostingu (Rootnode jest bardzo liberalne, ale, np. nie mogłem doprosić się o aktualizację Ruby do wersji 1.8.6, ponieważ ma "za dużo zależności"). Pamiętać trzeba oczywiście o tym, że samemu trzeba dbać o stabliność i bezpieczeństwo danych na takim serwerze, ale to z kolei wymusza pewną pracę i naukę, która później nie zginie.
PS. Jakby ktoś z czytających miał wiedzę na temat tego, jak ograniczyć zużycie pamięci przez mySQL, proszę o kontakt.
Poziom: 0 | Kategoria: Komputerowo-internetowo, Ruby, Techblog. | 4 komentarze
Na horyzoncie pojawił się nowy gracz na rynku serwerów do obsługi frameworków w języku Ruby i Python (w przyszłości) — jest nim Ebb. Serwer ten bije konkurentów na głowę, przynajmniej w szybkości i ilości requestów na sekundę. Ja zauważyłem, że zużywa troche więcej pamięci niż np. Mongrel. W swojej małej aplikacji używałem już Mongrela oraz młodszego Thina — nadszedł czas na przetestowanie Ebb. Na pierwszy rzut oka rzeczywiście wzrost wydajności widoczny jest niemal natychmiast.
Ebb jest na razie młodym serwerem i brak mu dokumentacji, a przynajmniej ja nie stwierdziłem takowej. Jednak aby odpalić aplikację w Railsach wystarczy wykonać kilka prostych poleceń:
$ gem install ebb
Następnie przejść do katalogu aplikacji Railsowej i odpalić
$ ebb_rails start
Pełna lista opcji, jakie oferuje Ebb jest następująca:
Usage: ebb_rails [options] start|stop
Server options:
-p, --port PORT use PORT (default: 3000)
-e, --env ENV Rails environment
(default: development)
-c, --chdir PATH Rails root dir
(default: current dir)
-d, --daemonize Daemonize
-l, --log-file FILE File to redirect output
-P, --pid-file FILE File to store PID
-t, --timeout SEC Request or command timeout in sec
(default: 60)
Common options:
-h, --help Show this message
-v, --version Show version
W przypadku uruchamiania jako demon, konieczne jest podanie ścieżki do pliku z PID procesu.